Я пытаюсь написать небольшую и простую библиотеку сопрограмм, чтобы лучше понять сопрограммы С++ 20. Кажется, это работает нормально, но когда я компилирую с помощью clang's address sanitizer, меня выдает.
Я сузил проблему до следующего примера кода (доступен с выходными данными компилятора и дезинфицирующего средства по адресу https://godbolt.org/z/WqY6Gd), но я до сих пор не могу в этом разобраться.
// namespace coro = std::/std::experimental;
// inlining this suppresses the error
__attribute__((noinline)) void foo(int& i) { i = 0; }
struct task {
struct promise_type {
promise_type() = default;
coro::suspend_always initial_suspend() noexcept { return {}; }
coro::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() noexcept { std::terminate(); }
void return_value(int v) noexcept { value = v; }
task get_return_object() {
return task{coro::coroutine_handle<promise_type>::from_promise(*this)};
}
int value{};
};
void Start() { return handle_.resume(); }
int Get() {
auto& promise = handle_.promise();
return promise.value;
}
coro::coroutine_handle<promise_type> handle_;
};
task func() { co_return 3; }
int main() {
auto t = func();
t.Start();
const auto result = t.Get();
foo(t.handle_.promise().value);
// moving this one line down or separating this into a noinline
// function suppresses the error
// removing this removes the stack-use-after-scope, but (rightfully) reports a leak
t.handle_.destroy();
if (result != 3) return 1;
}
Дезинфицирующее средство адресов сообщает об использовании после области действия (полный вывод доступен на сайте godbolt, ссылка выше). С некоторой помощью от lldb я обнаружил, что ошибка возникает в main, точнее: переход на строку 112 в листинге сборки, jne .LBB2_15
, переходит к отчету asan и никогда не возвращается. Кажется, это внутри пролога main
.
Как видно из комментариев, перемещение destroy()
на строку вниз или вызов его в отдельной функции noinline1 меняет поведение средства очистки адресов. Единственными двумя объяснениями этого, по-видимому, являются неопределенное поведение и ложное срабатывание asan (или сам -fsanitize=address
создает проблемы с продолжительностью жизни, что в некотором смысле одно и то же).
На данный момент я почти уверен, что в приведенном выше коде нет UB: и task
, и result
живут во фрейме основного стека, объект обещания живет во фрейме сопрограммы. Сам фрейм размещается (в стеке main из-за отсутствия точек приостановки) в строке 1 main и уничтожается прямо перед возвратом после последнего обращения к нему в foo()
. Фрейм сопрограммы не уничтожается автоматически, поскольку управление никогда не передается co_await final_suspend()
, согласно стандарт. Однако я некоторое время смотрел на этот код, поэтому, пожалуйста, простите меня, если я пропустил что-то очевидное.
Сборка, сгенерированная без санации, мне кажется, имеет смысл, и весь доступ к памяти происходит в пределах [rsp, rsp+24], как выделено. Кроме того, компиляция с помощью -fsanitize=address,undefined
, или просто -fsanitize=undefined
, или просто компиляция с помощью gcc с -fsanitize=address
не сообщает об ошибках, что наводит меня на мысль, что проблема скрыта где-то в коде, сгенерированном asan.
К сожалению, я не могу понять, что именно происходит в коде, созданном asan, и поэтому я публикую это. У меня есть общее представление об алгоритме дезинфекции адресов, но я не могу сопоставить сборку доступ/распределение памяти к тому, что происходит в коде C++.
Я надеюсь, что ответ поможет мне
- Понять, где спрятаны жизненные проблемы, если они есть
- Поймите, что именно происходит в
main
при компиляции с asan, чтобы человек, читающий это, мог иметь более ясный способ определить, какой доступ к памяти в коде C++ вызвал ошибку, и где (если где-нибудь) была выделена и освобождена эта память. - Последовательно подавляйте это конкретное ложное срабатывание и немного уточните, что его вызывает, если проблема действительно в асане.
Заранее спасибо.
1 Первоначально это привело меня к мысли, что оптимизатор clang считывает result
из (уничтоженного) фрейма сопрограммы напрямую, но перемещение destroy()
в деструктор задачи возвращает проблему и доказывает, что эта теория неверна, насколько я могу. сказать. destroy()
отсутствует в деструкторе в приведенном выше листинге, потому что он требует реализации конструкции/присваивания перемещения, чтобы избежать двойного освобождения, и я хотел, чтобы пример был как можно меньше и понятнее.