forked from OSchip/llvm-project
742 lines
28 KiB
ReStructuredText
742 lines
28 KiB
ReStructuredText
========================
|
|
Debugging C++ Coroutines
|
|
========================
|
|
|
|
.. contents::
|
|
:local:
|
|
|
|
Introduction
|
|
============
|
|
|
|
For performance and other architectural reasons, the C++ Coroutines feature in
|
|
the Clang compiler is implemented in two parts of the compiler. Semantic
|
|
analysis is performed in Clang, and Coroutine construction and optimization
|
|
takes place in the LLVM middle-end.
|
|
|
|
However, this design forces us to generate insufficient debugging information.
|
|
Typically, the compiler generates debug information in the Clang frontend, as
|
|
debug information is highly language specific. However, this is not possible
|
|
for Coroutine frames because the frames are constructed in the LLVM middle-end.
|
|
|
|
To mitigate this problem, the LLVM middle end attempts to generate some debug
|
|
information, which is unfortunately incomplete, since much of the language
|
|
specific information is missing in the middle end.
|
|
|
|
This document describes how to use this debug information to better debug
|
|
coroutines.
|
|
|
|
Terminology
|
|
===========
|
|
|
|
Due to the recent nature of C++20 Coroutines, the terminology used to describe
|
|
the concepts of Coroutines is not settled. This section defines a common,
|
|
understandable terminology to be used consistently throughout this document.
|
|
|
|
coroutine type
|
|
--------------
|
|
|
|
A `coroutine function` is any function that contains any of the Coroutine
|
|
Keywords `co_await`, `co_yield`, or `co_return`. A `coroutine type` is a
|
|
possible return type of one of these `coroutine functions`. `Task` and
|
|
`Generator` are commonly referred to coroutine types.
|
|
|
|
coroutine
|
|
---------
|
|
|
|
By technical definition, a `coroutine` is a suspendable function. However,
|
|
programmers typically use `coroutine` to refer to an individual instance.
|
|
For example:
|
|
|
|
.. code-block:: c++
|
|
|
|
std::vector<Task> Coros; // Task is a coroutine type.
|
|
for (int i = 0; i < 3; i++)
|
|
Coros.push_back(CoroTask()); // CoroTask is a coroutine function, which
|
|
// would return a coroutine type 'Task'.
|
|
|
|
In practice, we typically say "`Coros` contains 3 coroutines" in the above
|
|
example, though this is not strictly correct. More technically, this should
|
|
say "`Coros` contains 3 coroutine instances" or "Coros contains 3 coroutine
|
|
objects."
|
|
|
|
In this document, we follow the common practice of using `coroutine` to refer
|
|
to an individual `coroutine instance`, since the terms `coroutine instance` and
|
|
`coroutine object` aren't sufficiently defined in this case.
|
|
|
|
coroutine frame
|
|
---------------
|
|
|
|
The C++ Standard uses `coroutine state` to describe the allocated storage. In
|
|
the compiler, we use `coroutine frame` to describe the generated data structure
|
|
that contains the necessary information.
|
|
|
|
The structure of coroutine frames
|
|
=================================
|
|
|
|
The structure of coroutine frames is defined as:
|
|
|
|
.. code-block:: c++
|
|
|
|
struct {
|
|
void (*__r)(); // function pointer to the `resume` function
|
|
void (*__d)(); // function pointer to the `destroy` function
|
|
promise_type; // the corresponding `promise_type`
|
|
... // Any other needed information
|
|
}
|
|
|
|
In the debugger, the function's name is obtainable from the address of the
|
|
function. And the name of `resume` function is equal to the name of the
|
|
coroutine function. So the name of the coroutine is obtainable once the
|
|
address of the coroutine is known.
|
|
|
|
Print promise_type
|
|
==================
|
|
|
|
Every coroutine has a `promise_type`, which defines the behavior
|
|
for the corresponding coroutine. In other words, if two coroutines have the
|
|
same `promise_type`, they should behave in the same way.
|
|
To print a `promise_type` in a debugger when stopped at a breakpoint inside a
|
|
coroutine, printing the `promise_type` can be done by:
|
|
|
|
.. parsed-literal::
|
|
|
|
print __promise
|
|
|
|
It is also possible to print the `promise_type` of a coroutine from the address
|
|
of the coroutine frame. For example, if the address of a coroutine frame is
|
|
0x416eb0, and the type of the `promise_type` is `task::promise_type`, printing
|
|
the `promise_type` can be done by:
|
|
|
|
.. parsed-literal::
|
|
|
|
print (task::promise_type)*(0x416eb0+0x10)
|
|
|
|
This is possible because the `promise_type` is guaranteed by the ABI to be at a
|
|
16 bit offset from the coroutine frame.
|
|
|
|
Note that there is also an ABI independent method:
|
|
|
|
.. parsed-literal::
|
|
|
|
print std::coroutine_handle<task::promise_type>::from_address((void*)0x416eb0).promise()
|
|
|
|
The functions `from_address(void*)` and `promise()` are often small enough to
|
|
be removed during optimization, so this method may not be possible.
|
|
|
|
Print coroutine frames
|
|
======================
|
|
|
|
LLVM generates the debug information for the coroutine frame in the LLVM middle
|
|
end, which permits printing of the coroutine frame in the debugger. Much like
|
|
the `promise_type`, when stopped at a breakpoint inside a coroutine we can
|
|
print the coroutine frame by:
|
|
|
|
.. parsed-literal::
|
|
|
|
print __coro_frame
|
|
|
|
|
|
Just as printing the `promise_type` is possible from the coroutine address,
|
|
printing the details of the coroutine frame from an address is also possible:
|
|
|
|
::
|
|
|
|
(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 = {...}, ...}
|
|
|
|
The above is possible because:
|
|
|
|
(1) The name of the debug type of the coroutine frame is the `linkage_name`,
|
|
plus the `.coro_frame_ty` suffix because each coroutine function shares the
|
|
same coroutine type.
|
|
|
|
(2) The coroutine function name is accessible from the address of the coroutine
|
|
frame.
|
|
|
|
The above commands can be simplified by placing them in debug scripts.
|
|
|
|
Examples to print coroutine frames
|
|
----------------------------------
|
|
|
|
The print examples below use the following definition:
|
|
|
|
.. code-block:: c++
|
|
|
|
#include <coroutine>
|
|
#include <iostream>
|
|
|
|
struct task{
|
|
struct promise_type {
|
|
task get_return_object() { return std::coroutine_handle<promise_type>::from_promise(*this); }
|
|
std::suspend_always initial_suspend() { return {}; }
|
|
std::suspend_always final_suspend() noexcept { return {}; }
|
|
void return_void() noexcept {}
|
|
void unhandled_exception() noexcept {}
|
|
|
|
int count = 0;
|
|
};
|
|
|
|
void resume() noexcept {
|
|
handle.resume();
|
|
}
|
|
|
|
task(std::coroutine_handle<promise_type> hdl) : handle(hdl) {}
|
|
~task() {
|
|
if (handle)
|
|
handle.destroy();
|
|
}
|
|
|
|
std::coroutine_handle<> handle;
|
|
};
|
|
|
|
class await_counter : public std::suspend_always {
|
|
public:
|
|
template<class PromiseType>
|
|
void await_suspend(std::coroutine_handle<PromiseType> handle) noexcept {
|
|
handle.promise().count++;
|
|
}
|
|
};
|
|
|
|
static task coro_task(int v) {
|
|
int a = v;
|
|
co_await await_counter{};
|
|
a++;
|
|
std::cout << a << "\n";
|
|
a++;
|
|
std::cout << a << "\n";
|
|
a++;
|
|
std::cout << a << "\n";
|
|
co_await await_counter{};
|
|
a++;
|
|
std::cout << a << "\n";
|
|
a++;
|
|
std::cout << a << "\n";
|
|
}
|
|
|
|
int main() {
|
|
task t = coro_task(43);
|
|
t.resume();
|
|
t.resume();
|
|
t.resume();
|
|
return 0;
|
|
}
|
|
|
|
In debug mode (`O0` + `g`), the printing result would be:
|
|
|
|
.. parsed-literal::
|
|
|
|
{__resume_fn = 0x4019e0 <coro_task(int)>, __destroy_fn = 0x402000 <coro_task(int)>, __promise = {count = 1}, v = 43, a = 45, __coro_index = 1 '\001', struct_std__suspend_always_0 = {__int_8 = 0 '\000'},
|
|
class_await_counter_1 = {__int_8 = 0 '\000'}, class_await_counter_2 = {__int_8 = 0 '\000'}, struct_std__suspend_always_3 = {__int_8 = 0 '\000'}}
|
|
|
|
In the above, the values of `v` and `a` are clearly expressed, as are the
|
|
temporary values for `await_counter` (`class_await_counter_1` and
|
|
`class_await_counter_2`) and `std::suspend_always` (
|
|
`struct_std__suspend_always_0` and `struct_std__suspend_always_3`). The index
|
|
of the current suspension point of the coroutine is emitted as `__coro_index`.
|
|
In the above example, the `__coro_index` value of `1` means the coroutine
|
|
stopped at the second suspend point (Note that `__coro_index` is zero indexed)
|
|
which is the first `co_await await_counter{};` in `coro_task`. Note that the
|
|
first initial suspend point is the compiler generated
|
|
`co_await promise_type::initial_suspend()`.
|
|
|
|
However, when optimizations are enabled, the printed result changes drastically:
|
|
|
|
.. parsed-literal::
|
|
|
|
{__resume_fn = 0x401280 <coro_task(int)>, __destroy_fn = 0x401390 <coro_task(int)>, __promise = {count = 1}, __int_32_0 = 43, __coro_index = 1 '\001'}
|
|
|
|
Unused values are optimized out, as well as the name of the local variable `a`.
|
|
The only information remained is the value of a 32 bit integer. In this simple
|
|
case, it seems to be pretty clear that `__int_32_0` represents `a`. However, it
|
|
is not true.
|
|
|
|
An important note with optimization is that the value of a variable may not
|
|
properly express the intended value in the source code. For example:
|
|
|
|
.. code-block:: c++
|
|
|
|
static task coro_task(int v) {
|
|
int a = v;
|
|
co_await await_counter{};
|
|
a++; // __int_32_0 is 43 here
|
|
std::cout << a << "\n";
|
|
a++; // __int_32_0 is still 43 here
|
|
std::cout << a << "\n";
|
|
a++; // __int_32_0 is still 43 here!
|
|
std::cout << a << "\n";
|
|
co_await await_counter{};
|
|
a++; // __int_32_0 is still 43 here!!
|
|
std::cout << a << "\n";
|
|
a++; // Why is __int_32_0 still 43 here?
|
|
std::cout << a << "\n";
|
|
}
|
|
|
|
When debugging step-by-step, the value of `__int_32_0` seemingly does not
|
|
change, despite being frequently incremented, and instead is always `43`.
|
|
While this might be surprising, this is a result of the optimizer recognizing
|
|
that it can eliminate most of the load/store operations. The above code gets
|
|
optimized to the equivalent of:
|
|
|
|
.. code-block:: c++
|
|
|
|
static task coro_task(int v) {
|
|
store v to __int_32_0 in the frame
|
|
co_await await_counter{};
|
|
a = load __int_32_0
|
|
std::cout << a+1 << "\n";
|
|
std::cout << a+2 << "\n";
|
|
std::cout << a+3 << "\n";
|
|
co_await await_counter{};
|
|
a = load __int_32_0
|
|
std::cout << a+4 << "\n";
|
|
std::cout << a+5 << "\n";
|
|
}
|
|
|
|
It should now be obvious why the value of `__int_32_0` remains unchanged
|
|
throughout the function. It is important to recognize that `__int_32_0`
|
|
does not directly correspond to `a`, but is instead a variable generated
|
|
to assist the compiler in code generation. The variables in an optimized
|
|
coroutine frame should not be thought of as directly representing the
|
|
variables in the C++ source.
|
|
|
|
Get the suspended points
|
|
========================
|
|
|
|
An important requirement for debugging coroutines is to understand suspended
|
|
points, which are where the coroutine is currently suspended and awaiting.
|
|
|
|
For simple cases like the above, inspecting the value of the `__coro_index`
|
|
variable in the coroutine frame works well.
|
|
|
|
However, it is not quite so simple in really complex situations. In these
|
|
cases, it is necessary to use the coroutine libraries to insert the
|
|
line-number.
|
|
|
|
For example:
|
|
|
|
.. code-block:: c++
|
|
|
|
// For all the promise_type we want:
|
|
class promise_type {
|
|
...
|
|
+ unsigned line_number = 0xffffffff;
|
|
};
|
|
|
|
#include <source_location>
|
|
|
|
// For all the awaiter types we need:
|
|
class awaiter {
|
|
...
|
|
template <typename Promise>
|
|
void await_suspend(std::coroutine_handle<Promise> handle,
|
|
std::source_location sl = std::source_location::current()) {
|
|
...
|
|
handle.promise().line_number = sl.line();
|
|
}
|
|
};
|
|
|
|
In this case, we use `std::source_location` to store the line number of the
|
|
await inside the `promise_type`. Since we can locate the coroutine function
|
|
from the address of the coroutine, we can identify suspended points this way
|
|
as well.
|
|
|
|
The downside here is that this comes at the price of additional runtime cost.
|
|
This is consistent with the C++ philosophy of "Pay for what you use".
|
|
|
|
Get the asynchronous stack
|
|
==========================
|
|
|
|
Another important requirement to debug a coroutine is to print the asynchronous
|
|
stack to identify the asynchronous caller of the coroutine. As many
|
|
implementations of coroutine types store `std::coroutine_handle<> continuation`
|
|
in the promise type, identifying the caller should be trivial. The
|
|
`continuation` is typically the awaiting coroutine for the current coroutine.
|
|
That is, the asynchronous parent.
|
|
|
|
Since the `promise_type` is obtainable from the address of a coroutine and
|
|
contains the corresponding continuation (which itself is a coroutine with a
|
|
`promise_type`), it should be trivial to print the entire asynchronous stack.
|
|
|
|
This logic should be quite easily captured in a debugger script.
|
|
|
|
Examples to print asynchronous stack
|
|
------------------------------------
|
|
|
|
Here is an example to print the asynchronous stack for the normal task implementation.
|
|
|
|
.. code-block:: c++
|
|
|
|
// debugging-example.cpp
|
|
#include <coroutine>
|
|
#include <iostream>
|
|
#include <utility>
|
|
|
|
struct task {
|
|
struct promise_type {
|
|
task get_return_object();
|
|
std::suspend_always initial_suspend() { return {}; }
|
|
|
|
void unhandled_exception() noexcept {}
|
|
|
|
struct FinalSuspend {
|
|
std::coroutine_handle<> continuation;
|
|
auto await_ready() noexcept { return false; }
|
|
auto await_suspend(std::coroutine_handle<> handle) noexcept {
|
|
return continuation;
|
|
}
|
|
void await_resume() noexcept {}
|
|
};
|
|
FinalSuspend final_suspend() noexcept { return {continuation}; }
|
|
|
|
void return_value(int res) { result = res; }
|
|
|
|
std::coroutine_handle<> continuation = std::noop_coroutine();
|
|
int result = 0;
|
|
};
|
|
|
|
task(std::coroutine_handle<promise_type> handle) : handle(handle) {}
|
|
~task() {
|
|
if (handle)
|
|
handle.destroy();
|
|
}
|
|
|
|
auto operator co_await() {
|
|
struct Awaiter {
|
|
std::coroutine_handle<promise_type> handle;
|
|
auto await_ready() { return false; }
|
|
auto await_suspend(std::coroutine_handle<> continuation) {
|
|
handle.promise().continuation = continuation;
|
|
return handle;
|
|
}
|
|
int await_resume() {
|
|
int ret = handle.promise().result;
|
|
handle.destroy();
|
|
return ret;
|
|
}
|
|
};
|
|
return Awaiter{std::exchange(handle, nullptr)};
|
|
}
|
|
|
|
int syncStart() {
|
|
handle.resume();
|
|
return handle.promise().result;
|
|
}
|
|
|
|
private:
|
|
std::coroutine_handle<promise_type> handle;
|
|
};
|
|
|
|
task task::promise_type::get_return_object() {
|
|
return std::coroutine_handle<promise_type>::from_promise(*this);
|
|
}
|
|
|
|
namespace detail {
|
|
template <int N>
|
|
task chain_fn() {
|
|
co_return N + co_await chain_fn<N - 1>();
|
|
}
|
|
|
|
template <>
|
|
task chain_fn<0>() {
|
|
// This is the default breakpoint.
|
|
__builtin_debugtrap();
|
|
co_return 0;
|
|
}
|
|
} // namespace detail
|
|
|
|
task chain() {
|
|
co_return co_await detail::chain_fn<30>();
|
|
}
|
|
|
|
int main() {
|
|
std::cout << chain().syncStart() << "\n";
|
|
return 0;
|
|
}
|
|
|
|
In the example, the ``task`` coroutine holds a ``continuation`` field,
|
|
which would be resumed once the ``task`` completes.
|
|
In another word, the ``continuation`` is the asynchronous caller for the ``task``.
|
|
Just like the normal function returns to its caller when the function completes.
|
|
|
|
So we can use the ``continuation`` field to construct the asynchronous stack:
|
|
|
|
.. code-block:: python
|
|
|
|
# debugging-helper.py
|
|
import gdb
|
|
from gdb.FrameDecorator import FrameDecorator
|
|
|
|
class SymValueWrapper():
|
|
def __init__(self, symbol, value):
|
|
self.sym = symbol
|
|
self.val = value
|
|
|
|
def __str__(self):
|
|
return str(self.sym) + " = " + str(self.val)
|
|
|
|
def get_long_pointer_size():
|
|
return gdb.lookup_type('long').pointer().sizeof
|
|
|
|
def cast_addr2long_pointer(addr):
|
|
return gdb.Value(addr).cast(gdb.lookup_type('long').pointer())
|
|
|
|
def dereference(addr):
|
|
return long(cast_addr2long_pointer(addr).dereference())
|
|
|
|
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 memeber of promise_type.
|
|
self.continuation_addr = self.promise_addr
|
|
|
|
def next_task_addr(self):
|
|
return dereference(self.continuation_addr)
|
|
|
|
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 == None:
|
|
raise Exception('Not stackless coroutine.')
|
|
self.line_info = gdb.find_pc_line(self.resume_func)
|
|
|
|
def address(self):
|
|
return self.resume_func
|
|
|
|
def filename(self):
|
|
return self.line_info.symtab.filename
|
|
|
|
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))
|
|
]
|
|
|
|
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() == None else '%#x' % self.address() + " in "
|
|
location = "" if self.filename() == 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):
|
|
def __init__(self):
|
|
super(AsyncStack, self).__init__("async-bt", gdb.COMMAND_USER)
|
|
|
|
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)
|
|
|
|
frames = coroutine_filter.create_coroutine_frames(task)
|
|
i = 0
|
|
for f in frames:
|
|
print '#'+ str(i), str(StripDecorator(f))
|
|
i += 1
|
|
return
|
|
|
|
AsyncStack()
|
|
|
|
class ShowCoroFrame(gdb.Command):
|
|
def __init__(self):
|
|
super(ShowCoroFrame, self).__init__("show-coro-frame", gdb.COMMAND_USER)
|
|
|
|
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
|
|
|
|
addr = int(argv[0], 16)
|
|
block = gdb.block_for_pc(long(cast_addr2long_pointer(addr).dereference()))
|
|
if block == None:
|
|
print "block " + str(addr) + " is none."
|
|
return
|
|
|
|
# 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()
|
|
|
|
gdb.execute("set demangle-style auto")
|
|
gdb.write(coro_frame.format_string(pretty_structs = True))
|
|
|
|
ShowCoroFrame()
|
|
|
|
Then let's run:
|
|
|
|
.. code-block:: text
|
|
|
|
$ clang++ -std=c++20 -g debugging-example.cpp -o debugging-example
|
|
$ gdb ./debugging-example
|
|
(gdb) # We've alreay set the breakpoint.
|
|
(gdb) r
|
|
Program received signal SIGTRAP, Trace/breakpoint trap.
|
|
detail::chain_fn<0> () at debugging-example2.cpp:73
|
|
73 co_return 0;
|
|
(gdb) # Executes the debugging scripts
|
|
(gdb) source debugging-helper.py
|
|
(gdb) # Print the asynchronous stack
|
|
(gdb) async-bt
|
|
#0 0x401c40 in detail::chain_fn<0>() ['frame_addr = 0x441860', 'promise_addr = 0x441870', 'continuation_addr = 0x441870'] at debugging-example.cpp:71
|
|
#1 0x4022d0 in detail::chain_fn<1>() ['frame_addr = 0x441810', 'promise_addr = 0x441820', 'continuation_addr = 0x441820'] at debugging-example.cpp:66
|
|
#2 0x403060 in detail::chain_fn<2>() ['frame_addr = 0x4417c0', 'promise_addr = 0x4417d0', 'continuation_addr = 0x4417d0'] at debugging-example.cpp:66
|
|
#3 0x403df0 in detail::chain_fn<3>() ['frame_addr = 0x441770', 'promise_addr = 0x441780', 'continuation_addr = 0x441780'] at debugging-example.cpp:66
|
|
#4 0x404b80 in detail::chain_fn<4>() ['frame_addr = 0x441720', 'promise_addr = 0x441730', 'continuation_addr = 0x441730'] at debugging-example.cpp:66
|
|
#5 0x405910 in detail::chain_fn<5>() ['frame_addr = 0x4416d0', 'promise_addr = 0x4416e0', 'continuation_addr = 0x4416e0'] at debugging-example.cpp:66
|
|
#6 0x4066a0 in detail::chain_fn<6>() ['frame_addr = 0x441680', 'promise_addr = 0x441690', 'continuation_addr = 0x441690'] at debugging-example.cpp:66
|
|
#7 0x407430 in detail::chain_fn<7>() ['frame_addr = 0x441630', 'promise_addr = 0x441640', 'continuation_addr = 0x441640'] at debugging-example.cpp:66
|
|
#8 0x4081c0 in detail::chain_fn<8>() ['frame_addr = 0x4415e0', 'promise_addr = 0x4415f0', 'continuation_addr = 0x4415f0'] at debugging-example.cpp:66
|
|
#9 0x408f50 in detail::chain_fn<9>() ['frame_addr = 0x441590', 'promise_addr = 0x4415a0', 'continuation_addr = 0x4415a0'] at debugging-example.cpp:66
|
|
#10 0x409ce0 in detail::chain_fn<10>() ['frame_addr = 0x441540', 'promise_addr = 0x441550', 'continuation_addr = 0x441550'] at debugging-example.cpp:66
|
|
#11 0x40aa70 in detail::chain_fn<11>() ['frame_addr = 0x4414f0', 'promise_addr = 0x441500', 'continuation_addr = 0x441500'] at debugging-example.cpp:66
|
|
#12 0x40b800 in detail::chain_fn<12>() ['frame_addr = 0x4414a0', 'promise_addr = 0x4414b0', 'continuation_addr = 0x4414b0'] at debugging-example.cpp:66
|
|
#13 0x40c590 in detail::chain_fn<13>() ['frame_addr = 0x441450', 'promise_addr = 0x441460', 'continuation_addr = 0x441460'] at debugging-example.cpp:66
|
|
#14 0x40d320 in detail::chain_fn<14>() ['frame_addr = 0x441400', 'promise_addr = 0x441410', 'continuation_addr = 0x441410'] at debugging-example.cpp:66
|
|
#15 0x40e0b0 in detail::chain_fn<15>() ['frame_addr = 0x4413b0', 'promise_addr = 0x4413c0', 'continuation_addr = 0x4413c0'] at debugging-example.cpp:66
|
|
#16 0x40ee40 in detail::chain_fn<16>() ['frame_addr = 0x441360', 'promise_addr = 0x441370', 'continuation_addr = 0x441370'] at debugging-example.cpp:66
|
|
#17 0x40fbd0 in detail::chain_fn<17>() ['frame_addr = 0x441310', 'promise_addr = 0x441320', 'continuation_addr = 0x441320'] at debugging-example.cpp:66
|
|
#18 0x410960 in detail::chain_fn<18>() ['frame_addr = 0x4412c0', 'promise_addr = 0x4412d0', 'continuation_addr = 0x4412d0'] at debugging-example.cpp:66
|
|
#19 0x4116f0 in detail::chain_fn<19>() ['frame_addr = 0x441270', 'promise_addr = 0x441280', 'continuation_addr = 0x441280'] at debugging-example.cpp:66
|
|
#20 0x412480 in detail::chain_fn<20>() ['frame_addr = 0x441220', 'promise_addr = 0x441230', 'continuation_addr = 0x441230'] at debugging-example.cpp:66
|
|
#21 0x413210 in detail::chain_fn<21>() ['frame_addr = 0x4411d0', 'promise_addr = 0x4411e0', 'continuation_addr = 0x4411e0'] at debugging-example.cpp:66
|
|
#22 0x413fa0 in detail::chain_fn<22>() ['frame_addr = 0x441180', 'promise_addr = 0x441190', 'continuation_addr = 0x441190'] at debugging-example.cpp:66
|
|
#23 0x414d30 in detail::chain_fn<23>() ['frame_addr = 0x441130', 'promise_addr = 0x441140', 'continuation_addr = 0x441140'] at debugging-example.cpp:66
|
|
#24 0x415ac0 in detail::chain_fn<24>() ['frame_addr = 0x4410e0', 'promise_addr = 0x4410f0', 'continuation_addr = 0x4410f0'] at debugging-example.cpp:66
|
|
#25 0x416850 in detail::chain_fn<25>() ['frame_addr = 0x441090', 'promise_addr = 0x4410a0', 'continuation_addr = 0x4410a0'] at debugging-example.cpp:66
|
|
#26 0x4175e0 in detail::chain_fn<26>() ['frame_addr = 0x441040', 'promise_addr = 0x441050', 'continuation_addr = 0x441050'] at debugging-example.cpp:66
|
|
#27 0x418370 in detail::chain_fn<27>() ['frame_addr = 0x440ff0', 'promise_addr = 0x441000', 'continuation_addr = 0x441000'] at debugging-example.cpp:66
|
|
#28 0x419100 in detail::chain_fn<28>() ['frame_addr = 0x440fa0', 'promise_addr = 0x440fb0', 'continuation_addr = 0x440fb0'] at debugging-example.cpp:66
|
|
#29 0x419e90 in detail::chain_fn<29>() ['frame_addr = 0x440f50', 'promise_addr = 0x440f60', 'continuation_addr = 0x440f60'] at debugging-example.cpp:66
|
|
#30 0x41ac20 in detail::chain_fn<30>() ['frame_addr = 0x440f00', 'promise_addr = 0x440f10', 'continuation_addr = 0x440f10'] at debugging-example.cpp:66
|
|
#31 0x41b9b0 in chain() ['frame_addr = 0x440eb0', 'promise_addr = 0x440ec0', 'continuation_addr = 0x440ec0'] at debugging-example.cpp:77
|
|
|
|
Now we get the complete asynchronous stack!
|
|
It is also possible to print other asynchronous stack which doesn't live in the top of the stack.
|
|
We can make it by passing the address of the corresponding coroutine frame to ``async-bt`` command.
|
|
|
|
By the debugging scripts, we can print any coroutine frame too as long as we know the address.
|
|
For example, we can print the coroutine frame for ``detail::chain_fn<18>()`` in the above example.
|
|
From the log record, we know the address of the coroutine frame is ``0x4412c0`` in the run. Then we can:
|
|
|
|
.. code-block:: text
|
|
|
|
(gdb) show-coro-frame 0x4412c0
|
|
{
|
|
__resume_fn = 0x410960 <detail::chain_fn<18>()>,
|
|
__destroy_fn = 0x410d60 <detail::chain_fn<18>()>,
|
|
__promise = {
|
|
continuation = {
|
|
_M_fr_ptr = 0x441270
|
|
},
|
|
result = 0
|
|
},
|
|
struct_Awaiter_0 = {
|
|
struct_std____n4861__coroutine_handle_0 = {
|
|
struct_std____n4861__coroutine_handle = {
|
|
PointerType = 0x441310
|
|
}
|
|
}
|
|
},
|
|
struct_task_1 = {
|
|
struct_std____n4861__coroutine_handle_0 = {
|
|
struct_std____n4861__coroutine_handle = {
|
|
PointerType = 0x0
|
|
}
|
|
}
|
|
},
|
|
struct_task__promise_type__FinalSuspend_2 = {
|
|
struct_std____n4861__coroutine_handle = {
|
|
PointerType = 0x0
|
|
}
|
|
},
|
|
__coro_index = 1 '\001',
|
|
struct_std____n4861__suspend_always_3 = {
|
|
__int_8 = 0 '\000'
|
|
}
|
|
|
|
|
|
Get the living coroutines
|
|
=========================
|
|
|
|
Another useful task when debugging coroutines is to enumerate the list of
|
|
living coroutines, which is often done with threads. While technically
|
|
possible, this task is not recommended in production code as it is costly at
|
|
runtime. One such solution is to store the list of currently running coroutines
|
|
in a collection:
|
|
|
|
.. code-block:: c++
|
|
|
|
inline std::unordered_set<void*> lived_coroutines;
|
|
// For all promise_type we want to record
|
|
class promise_type {
|
|
public:
|
|
promise_type() {
|
|
// Note to avoid data races
|
|
lived_coroutines.insert(std::coroutine_handle<promise_type>::from_promise(*this).address());
|
|
}
|
|
~promise_type() {
|
|
// Note to avoid data races
|
|
lived_coroutines.erase(std::coroutine_handle<promise_type>::from_promise(*this).address());
|
|
}
|
|
};
|
|
|
|
In the above code snippet, we save the address of every lived coroutine in the
|
|
`lived_coroutines` `unordered_set`. As before, once we know the address of the
|
|
coroutine we can derive the function, `promise_type`, and other members of the
|
|
frame. Thus, we could print the list of lived coroutines from that collection.
|
|
|
|
Please note that the above is expensive from a storage perspective, and requires
|
|
some level of locking (not pictured) on the collection to prevent data races.
|