代码之家  ›  专栏  ›  技术社区  ›  zneak

如何创建并行堆栈并在其上运行协同程序?

  •  11
  • zneak  · 技术社区  · 14 年前

    我决定我应该尝试实现协同程序(我想我应该这样称呼它们)以获得乐趣和利润。我希望必须使用汇编语言,如果我想让它对任何东西都有用的话,可能还需要一些C语言。

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

    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
    

    我想要的是一种在不同堆栈上运行两个函数的方法,不需要线程(显然,一次只能跑一次。这两个函数必须能够恢复另一个函数的执行(并停止它们自己的执行)。有点像是 长JMP 另一个是我。一旦它返回到另一个函数,它就必须恢复到它离开的位置(即,在对另一个函数进行控制的调用期间或之后),有点像 长JMP .

    1. 功能 A 创建并行堆栈并将其归零(分配内存等)。
    2. 功能 A 将其所有寄存器推送到当前堆栈。
    3. 功能 将堆栈指针和基指针设置为该新位置,并将 神秘的数据结构 指示要跳回的位置以及要将指令指针设置回的位置。
    4. A B .

    1. 功能 B 在那堆上工作,做任何需要的工作。
    2. 功能 到了需要中断和给予的地步 再次控制。
    3. B 神秘的数据结构 A 新的,改良的 数据结构 .
    4. 功能 A B 再次控制。

    我觉得这一切都不错。然而,有一些事情我并不完全放心。

    • 显然,在好的ol'x86上,有这样一个 pusha 将所有寄存器发送到堆栈的指令。然而,处理器体系结构不断发展,现在有了x86\u64,我们有了更多的通用寄存器,可能还有几个SSE寄存器。我找不到任何证据证明 确实推动了他们。现代x86cpu中大约有40个公共寄存器。我必须做所有的事情吗 push 对于SSE寄存器(虽然肯定会有一个等价的,但我对整个“x86汇编程序”还不熟悉)。
    • 改变指令指针和说它一样容易吗?我能做,比如, mov rip, rax (英特尔语法)?而且,从中获得价值必须有点特殊,因为它不断变化。如果我真的喜欢 mov rax, rip (又是英特尔语法),威尔 rip mov 指令,它后面的指令,还是介于两者之间? 只是 jmp foo
    • 我提到了一个 神秘的数据结构 几次。到目前为止,我假设它至少需要包含三种内容:基指针、堆栈指针和指令指针。还有别的事吗?
    • 我忘了什么吗?
    • 理解 如何工作,我敢肯定有一些图书馆就是这样做的。你知道吗?有没有任何POSIX或BSD定义的标准方法来实现它,比如 pthread

    谢谢你看我的报告 问题

    4 回复  |  直到 10 年前
        1
  •  9
  •   L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳    14 年前

    你说得对 PUSHA #UD ,作为 普沙 按下16位或32位通用寄存器。看到了吗 Intel manuals

    设置 RIP jmp rax RAX

    a:
    call b
    b:
    pop 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 是指向协程函数体的指针。

    此函数执行以下操作:

    1. 分配 协同程序状态 结构 cs
    2. 分配 初始化参数 cs.initarg
    3. 分配 coro_func公司 cs.registers.rip
    4. 将当前标志复制到 cs.registers
    5. 为协同程序的堆栈分配一些适当大小的区域,并将其分配给 cs.registers.rsp
    6. 协同程序状态 结构

    现在我们有另一个功能:

    void* coroutine_next(coroutine_state cs, void* arg)

    哪里 反恐精英 结构是否从 coroutine_init 精氨酸 将在协同程序恢复执行时输入到协同程序中。

    1. 将所有当前标志/寄存器存储在 cs.caller_registers 除了 RSP ,参见步骤3。
    2. 精氨酸 在里面 cs.arg
    3. 修复调用程序堆栈指针( cs.caller_registers.rsp ),添加 2*sizeof(void*) 如果你幸运的话,你会修复它,你必须查这个来确认它,你可能希望这个函数是stdcall,这样在调用它之前没有寄存器被篡改
    4. mov rax, [rsp] ,分配 雷克斯 cs.caller_registers.rip ; 说明:除非你的编译器已经崩溃了, [RSP] 将保留指向调用此函数的调用指令后面的指令的指令指针(即:返回地址)
    5. 从加载标志和寄存器
    6. jmp cs.registers.rip ,有效地恢复了协同程序的执行

    协程产量 ). 还要注意的是,在这个函数中,您可能会遇到许多复杂的问题,例如C编译器生成的函数序言和尾声,以及可能的寄存器参数,您必须处理好所有这些问题。就像我说的,stdcall会救你的 麻烦的是,我认为gcc的-fomit-frame\u指针将删除尾声的内容。

    最后一个函数声明为:

    void coroutine_yield(void* ret);

    这个函数在协程中被调用,以“暂停”协程的执行并返回给 coroutine_next .

    1. 存储标志/寄存器 in cs.registers
    2. 修复协程堆栈指针( ),再次添加 2*尺寸(空*) 你也希望这个函数也是stdcall
    3. mov rax, arg (让我们假设编译器中的所有函数都在 雷克斯 )
    4. 从加载标志/寄存器 cs.U寄存器
    5. jmp cs.caller_registers.rip 这基本上是从 调用协程调用程序的堆栈帧,因为返回值被传入 雷克斯 ,我们回来了 . 我们就说如果 NULL ,则协程终止,否则它是任意数据结构。

    协同程序初始化 ,然后可以使用 .

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

    保存初始函数参数(想想构造函数)。每次 my_coro cs.arg公司 . 这就是协同程序调用程序与协同程序通信的方式。最后,每次协同程序想暂停自己时,它都会调用 协程产量 ,并将一个参数传递给它,这是到协同程序调用器的返回值。

    好吧,你现在可能会想“那很简单!”,但是我忽略了以正确的顺序加载寄存器和标志的所有复杂操作,同时仍然保持一个非损坏的堆栈框架,并以线程安全的方式保持协程数据结构的地址(您只需重写所有寄存器)。对于这一部分,您需要了解编译器如何在内部工作。。。祝你好运:)

        2
  •  1
  •   Yann Ramin    14 年前

    libcoroutine 尤其是它们的setjmp/longjmp实现。我知道使用现有的图书馆并不有趣,但你至少可以大致了解你要去哪里。

        3
  •  1
  •   caf    14 年前

    西蒙·塔塔姆有一个 interesting implementation of coroutines in C 这不需要任何特定于体系结构的知识或堆栈摆弄。这并不完全是你想要的,但我想这至少会引起学术界的兴趣。

        4
  •  -1
  •   olk    11 年前

    boost.org上的boost.coroutine(boost.context)为您做所有的事情