проблема с переключением фрейма стека

#gcc #assembly #c 14 #setjmp

#gcc #сборка #c 14 #setjmp

Вопрос:

Почему происходит сбой следующего кода в Windows 10? Он застревает yield() в setjmp() вызове. Какие правила я нарушил? Я думаю, что я никогда не возвращаюсь из функций, где setjmp() вызывается. Код отлично работает под linux / amd64.

 class coroutine
{
  jmp_buf env_in_;
  jmp_buf env_out_;

  ::std::function<void()> f_;

  bool running_;
  bool terminated_;

  ::std::unique_ptr<char[]> stack_;

  char* const stack_top_;

public:
  explicit coroutine(::std::size_t const N = 128 * 1024) :
    running_{false},
    terminated_{true},
    stack_(new char[N]),
    stack_top_(stack_.get()   N)
  {
  }

  template <typename F>
  explicit coroutine(::std::size_t const N, Famp;amp; f) :
    coroutine(N)
  {
    assign(::std::forward<F>(f));
  }

  auto terminated() const noexcept
  {
    return terminated_;
  }

  template <typename F>
  void assign(Famp;amp; f)
  {
    running_ = terminated_ = false;

    f_ = [this, f = ::std::forward<F>(f)]() mutable 
      {
        // stack switch
#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64)
        asm volatile(
          "movq %%rsp, %0"
          :
          : "rm" (stack_top_)
          : "rsp"
        );
#elif defined(i386) || defined(__i386) || defined(__i386__)
        asm volatile(
          "movl %%esp, %0"
          :
          : "rm" (stack_top_)
          : "esp"
        );
#else
#error "can't switch stack frame"
#endif

        f(*this);

        running_ = false;

        terminated_ = true;

        yield();
      };
  }

  void yield() noexcept
  {
    if (setjmp(env_out_))
    {
      return;
    }
    else
    {
      longjmp(env_in_, 1);
    }
  }

  void resume() noexcept
  {
    if (setjmp(env_in_))
    {
      return;
    }
    else if (running_)
    {
      longjmp(env_out_, 1);
    }
    else
    {
      running_ = true;

      f_();
    }
  }
};
 

Вот тестовая программа:

 #include <iostream>

#include "coroutine.hpp"

struct A
{
  ~A()
  {
    ::std::cout << "destroyed" << ::std::endl;
  }
};

int main()
{
  coroutine c(1024 * 1024);

  c.assign([](coroutineamp; c)
    {
      A a;

      for (int i{}; i != 3;   i)
      {
        ::std::cout << i << ::std::endl;

        c.yield();
      }
    }
  );

  while (!c.terminated())
  {
    c.resume();
  }

  return 0;
}
 

Комментарии:

1. В синтаксисе At amp; t операнды перевернуты, поэтому вы фактически не переключаете стек, а сохраняете указатель стека. Это, вероятно, не вызывает ошибку. Я не думаю, что добавление указателя стека в clobber также имеет смысл, поскольку это может дать указание компилятору сохранить и восстановить его, отрицая ваш переключатель (даже если вы делаете это в правильном направлении). Также обратите внимание, что это new char[] может привести к неправильному выравниванию вашего стека.

2. @Jester Ваша точка зрения была правильной, но проблема сохраняется. Могу ли я изменить вопрос? Разве new не возвращает указатель, выровненный по каждому выравниванию?

3. @Jester Сглаживание указателя стека не имеет никакого эффекта ( gcc.gnu.org/bugzilla/show_bug.cgi?id=52813 ). OTOH, переключение контекстов вызывает вопрос обо всех других регистрах. Возможно, вместо этого все они должны быть заблокированы (вместе с памятью и флагами).

4. Именно, в этом и заключалась проблема. Другие регистры не были восстановлены. Пользовательский setjmp / longjmp решил это.