c - 如何创建并行堆栈并在其上运行协程?

标签 c assembly stack x86-64 coroutine

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

我是这样想的:
  • 功能 A创建并行堆栈并将其归零(分配内存和所有这些)。
  • 功能 A将其所有寄存器压入当前堆栈。
  • 功能 A将堆栈指针和基指针设置到该新位置,并推送一个神秘的数据结构,指示从哪里跳回以及将指令指针设置回哪里。
  • 功能 A将其大部分寄存器清零并将指令指针设置为函数 B 的开头.

  • 那是为了初始化。现在,以下情况将无限循环:
  • 功能 B在那个堆栈上工作,做任何它需要的工作。
  • 功能 B到了需要中断并给出 A 的地步再次控制。
  • 功能 B将所有寄存器压入堆栈,获取神秘的数据结构 A一开始就给了,并将堆栈指针和指令指针设置为A告诉它。在这个过程中,它交还A一个新的、修改过的数据结构,告诉在哪里恢复 B .
  • 功能 A唤醒,弹出它推送到其堆栈的所有寄存器,并一直工作直到需要中断并给出 B再次控制。

  • 这一切对我来说听起来不错。然而,有很多事情我并不完全放心。
  • 显然,在好的 ol' x86 上,有这个 pusha将所有寄存器发送到堆栈的指令。然而,处理器架构不断发展,现在有了 x86_64,我们有了更多的通用寄存器,可能还有几个 SSE 寄存器。我找不到任何证据表明 pusha确实插入了他们。现代 x86 CPU 中大约有 40 个公共(public)寄存器。我必须做所有的push是我自己吗?而且,没有push对于 SSE 寄存器(尽管肯定会有一个等效项——我对整个“x86 汇编程序”不熟悉)。
  • 更改指令指针是否像说的那么容易?我可以做,比如,mov rip, rax (英特尔语法)?此外,从它获取值必须有些特殊,因为它不断变化。如果我喜欢 mov rax, rip (再次英特尔语法),将 rip定位于mov指令,到它之后的指令,还是介于两者之间?只是jmp foo .假的。
  • 我已经多次提到一个神秘的数据结构。到目前为止,我认为它至少需要包含三样东西:基指针、堆栈指针和指令指针。还有别的事吗?
  • 我忘记了什么吗?
  • 虽然我真的很想了解事情是如何工作的,但我很确定有一些库可以做到这一点。你知道任何?是否有任何 POSIX 或 BSD 定义的标准方法可以做到这一点,例如 pthread对于线程?

  • 感谢您阅读我的问题 textwall。

    最佳答案

    你是对的 PUSHA不适用于 x64,它会引发异常 #UD , 如 PUSHA只推送 16 位或 32 位通用寄存器。见 Intel manuals获取您想知道的所有信息。

    设置 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);
    哪里coro_func是一个指向协程函数体的指针。

    该函数执行以下操作:
  • 分配一个 coroutine_state结构 cs
  • 分配 initargcs.initarg ,这些将是协程
  • 的初始参数
  • 分配 coro_funccs.registers.rip
  • 将当前标志复制到 cs.registers (不是寄存器,只有标志,因为我们需要一些理智的标志来防止世界末日)
  • 为协程的堆栈分配一些合适大小的区域并将其分配给 cs.registers.rsp
  • 返回指向分配的指针 coroutine_state结构

  • 现在我们有另一个功能:
    void* coroutine_next(coroutine_state cs, void* arg)
    哪里cs是从 coroutine_init 返回的结构代表一个协程实例,以及 arg将在协程恢复执行时被送入协程。

    该函数由协程调用者调用,以向协程传递一些新参数并恢复它,该函数的返回值是协程返回(产生)的任意数据结构。
  • 将所有当前标志/寄存器存储在 cs.caller_registers 中除了 RSP ,请参阅步骤 3。
  • 存储 argcs.arg
  • 修复调用者堆栈指针( cs.caller_registers.rsp ),添加 2*sizeof(void*)如果幸运的话会修复它,你必须查看它以确认它,你可能希望这个函数是 stdcall 以便在调用它之前没有寄存器被篡改
  • mov rax, [rsp] , 分配 RAXcs.caller_registers.rip ;解释:除非你的编译器是破解版,[RSP]将持有指向调用此函数的调用指令之后的指令的指令指针(即:返回地址)
  • cs.registers 加载标志和寄存器
  • jmp cs.registers.rip ,有效地恢复协程的执行

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

    最后一个函数声明为:
    void coroutine_yield(void* ret);
    该函数在协程内部调用以“暂停”协程的执行并返回到coroutine_next的调用者。 .
  • 存储标志/寄存器 in cs.registers
  • 修复协程堆栈指针( cs.registers.rsp ),再次添加 2*sizeof(void*)到它,并且你希望这个函数也是 stdcall
  • mov rax, arg (让我们假设编译器中的所有函数都在 RAX 中返回它们的参数)
  • cs.caller_registers 加载标志/寄存器
  • 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 , 并向它传递一个参数,即协程调用程序的返回值。

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

    关于c - 如何创建并行堆栈并在其上运行协程?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3089841/

    相关文章:

    c - 使用CBLAS的外部产品

    c - 如何用 lseek() 判断偏移光标是否在 EOF 处?

    objective-c - 为什么 clang 优化会破坏我的内联汇编代码?

    c - 程序集 : Returning 64 bits pointer address (nasm unix x64)

    c - 局部变量内存和分配的内存位置

    c - 为什么 gcc 为 memcpy 复制 rodata 字符串?如何避免?

    c - 如何计算重复的整数

    c - 程序集 MMX 点积段错误

    python Pandas : group by several columns and count value for one column

    c++ - 如果堆栈在数值较低的地址增长,为什么指针比较会反转它?