12

我决定我应该尝试实现协程(我认为我应该这样称呼它们)以获得乐趣和利润。我希望必须使用汇编程序,如果我想让它对任何事情真正有用,可能还需要一些 C。

请记住,这是出于教育目的。使用已经构建的协程库太容易了(而且真的没有乐趣)。

你们知道setjmplongjmp?它们允许您将堆栈展开到预定义的位置,并从那里恢复执行。但是,它不能倒退到堆栈上的“稍后”。只能早点回来。

jmpbuf_t checkpoint;
int retval = setjmp(&checkpoint); // returns 0 the first time
/* lots of stuff, lots of calls, ... We're not even in the same frame anymore! */
longjmp(checkpoint, 0xcafebabe); // execution resumes where setjmp is, and now it returns 0xcafebabe instead of 0

我想要的是一种无需线程即可在不同堆栈上运行两个函数的方法。(显然,一次只运行一个。没有线程,我说过。)这两个函数必须能够恢复另一个的执行(并停止它们自己的)。有点像他们在longjmp对对方说话。一旦它返回到另一个函数,它必须从它离开的地方恢复(也就是说,在将控制权交给另一个函数的调用期间或之后),有点像如何longjmp返回到setjmp.

我是这样想的:

  1. 函数A创建并行堆栈并将其归零(分配内存等)。
  2. 函数A将其所有寄存器推入当前堆栈。
  3. 函数A将堆栈指针和基指针设置到该新位置,并推送一个神秘的数据结构,指示在哪里跳转以及在哪里设置指令指针。
  4. 函数A将其大部分寄存器归零并将指令指针设置为函数的开头B

那是为了初始化。现在,以下情况将无限循环:

  1. 函数B在那个堆栈上工作,做它需要做的任何工作。
  2. 函数到了需要中断并再次给予控制B的地步。A
  3. 函数B将其所有寄存器推入堆栈,获取一开始给它的神秘数据结构 A,并将堆栈指针和指令指针设置为A告诉它的位置。在这个过程中,它交还A一个新的、修改过的数据结构,告诉在哪里恢复B
  4. 函数A被唤醒,将它压入堆栈的所有寄存器弹回,并继续工作,直到它需要中断并B再次给予控制权。

这一切对我来说听起来不错。然而,有很多事情我并不完全放心。

  • 显然,在好的 ol' x86 上,有这pusha条指令会将所有寄存器发送到堆栈。然而,处理器架构不断发展,现在有了 x86_64,我们有了更多的通用寄存器,并且可能还有几个 SSE 寄存器。我找不到任何pusha确实推动他们的证据。现代 x86 CPU 中大约有 40 个公共寄存器。我必须自己做所有的pushes吗?此外,没有push适用于 SSE 的寄存器(尽管肯定会有一个等价的——我对整个“x86 汇编器”这个东西是新手)。
  • 改变指令指针就这么简单吗?我可以这样做mov rip, rax(英特尔语法)吗?此外,由于它不断变化,因此从中获取价值必须有些特殊。如果我喜欢mov rax, rip(再次使用 Intel 语法),将rip定位在mov指令上、指令之后,还是介于两者之间?只是jmp foo。假的。
  • 我曾多次提到一种神秘的数据结构。到目前为止,我假设它至少需要包含三件事:基指针、堆栈指针和指令指针。还有别的事吗?
  • 我忘了什么吗?
  • 虽然我真的很想了解事情是如何工作的,但我很确定有一些库可以做到这一点。你知道任何?是否有任何 POSIX 或 BSD 定义的标准方法,比如pthread线程?

感谢您阅读我的问题文字墙。

4

4 回答 4

9

你是对的,因为PUSHA它不会在 x64 上工作,它会引发异常#UD,因为PUSHA 推送 16 位或 32 位通用寄存器。有关您想知道的所有信息,请参阅英特尔手册。

设置RIP很简单,jmp rax将设置RIPRAX。要检索 RIP,如果您已经知道所有协程出口来源,您可以在编译时获取它,或者您可以在运行时获取它,您可以在调用之后调用下一个地址。像这样:

a:
call b
b:
pop rax

RAX现在将b。这是有效的,因为CALL推送了下一条指令的地址。这种技术也适用于 IA32(尽管我认为在 x64 上有更好的方法,因为它支持 RIP 相对寻址,但我不知道有哪一种)。当然,如果你做一个函数coroutine_yield,它可以拦截调用者地址:)

由于您不能在一条指令中将所有寄存器推入堆栈,因此我不建议将协程状态存储在堆栈上,因为无论如何这会使事情复杂化。我认为最好的办法是为每个协程实例分配一个数据结构。

你为什么要把函数归零A?这可能没有必要。

以下是我将如何处理整个事情,试图使其尽可能简单:

创建一个coroutine_state包含以下内容的结构:

  • initarg
  • arg
  • registers(还包含标志)
  • caller_registers

创建一个函数:

coroutine_state* coroutine_init(void (*coro_func)(coroutine_state*), void* initarg);

wherecoro_func是指向协程函数体的指针。

此函数执行以下操作:

  1. 分配一个coroutine_state结构cs
  2. 分配initargcs.initarg,这些将是协程的初始参数
  3. 分配coro_funccs.registers.rip
  4. 将当前标志复制到cs.registers(不是寄存器,只有标志,因为我们需要一些理智的标志来防止天启)
  5. 为协程的堆栈分配一些大小合适的区域并将其分配给cs.registers.rsp
  6. 返回指向已分配coroutine_state结构的指针

现在我们有另一个功能:

void* coroutine_next(coroutine_state cs, void* arg)

哪里cs是从它返回的结构,coroutine_init它代表一个协程实例,并将arg在协程恢复执行时被馈送到协程中。

该函数由协程调用者调用,以将一些新参数传递给协程并恢复它,该函数的返回值是协程返回(生成)的任意数据结构。

  1. 将所有当前标志/寄存器存储在cs.caller_registers除 之外RSP,请参见步骤 3。
  2. 存储argcs.arg
  3. cs.caller_registers.rsp修复调用者2*sizeof(void*)堆栈指针(
  4. mov rax, [rsp], 分配RAXcs.caller_registers.rip; 解释:除非您的编译器已破解,[RSP]否则将保存指向调用此函数的 call 指令后面的指令的指令指针(即:返回地址)
  5. 从加载标志和寄存器cs.registers
  6. jmp cs.registers.rip,有效地恢复协程的执行

请注意,我们永远不会从这个函数返回,我们跳转到的协程为我们“返回”(参见 参考资料coroutine_yield)。另请注意,在此函数中,您可能会遇到许多复杂情况,例如 C 编译器生成的函数序言和结尾,可能还有注册参数,您必须处理所有这些。就像我说的,stdcall 会为你省去很多麻烦,我认为 gcc 的 -fomit-frame_pointer 会删除结尾的东西。

最后一个函数声明为:

void coroutine_yield(void* ret);

该函数在协程内部被调用以“暂停”协程的执行并返回给coroutine_next.

  1. 存储标志/寄存器in cs.registers
  2. 修复协程堆栈指针 ( cs.registers.rsp),再次添加2*sizeof(void*),并且您希望此函数也为 stdcall
  3. mov rax, arg(让我们假设编译器中的所有函数都返回它们的参数RAX
  4. 从加载标志/寄存器cs.caller_registers
  5. jmp cs.caller_registers.rip这实质上是从coroutine_next协程调用者的堆栈帧上的调用返回的,并且由于返回值是传入的RAX,所以我们返回了arg。让我们说如果argNULL,那么协程已经终止,否则它是一个任意数据结构。

所以回顾一下,你使用 初始化一个协程coroutine_init,然后你可以用 重复调用实例化的协程coroutine_next

协程的函数本身被声明为: void my_coro(coroutine_state cs)

cs.initarg保存初始函数参数(想想构造函数)。每次my_coro调用时,cs.arg都有一个由 指定的不同参数coroutine_next。这就是协程调用者与协程通信的方式。最后,每次协程想要暂停自己时,它都会调用coroutine_yield,并将一个参数传递给它,这是协程调用者的返回值。

好的,你现在可能认为“这很容易!”,但我忽略了以正确顺序加载寄存器和标志的所有复杂性,同时仍然保持未损坏的堆栈帧并以某种方式保持协程数据结构的地址(你只是以线程安全的方式覆盖了所有寄存器)。对于那部分,您将需要了解您的编译器如何在内部工作......祝你好运:)

于 2010-06-22T05:38:20.763 回答
1

很好的学习参考:libcoroutine,尤其是他们的 setjmp/longjmp 实现。我知道使用现有的库并不好玩,但你至少可以大致了解你要去哪里。

于 2010-06-22T02:30:37.140 回答
1

Simon Tatham 有一个有趣的 C 协程实现,它不需要任何特定于架构的知识或堆栈摆弄。这不完全是你所追求的,但我认为它可能至少具有学术兴趣。

于 2010-06-22T04:57:40.707 回答
-2

boost.org 上的 boost.coroutine (boost.context) 为您完成了一切

于 2013-03-11T12:10:19.227 回答