你是对的PUSHA
不能在 x64 上工作,它会引发异常#UD
, as PUSHA
only压入 16 位或 32 位通用寄存器。请参阅英特尔手册 http://www.intel.com/products/processor/manuals/获取您想了解的所有信息。
Setting RIP
很简单,jmp rax
将设置RIP
to RAX
。要检索 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);
where coro_func
是指向协程函数体的指针。
该函数执行以下操作:
- 分配一个
coroutine_state
结构cs
- assign
initarg
to cs.initarg
,这些将是协程的初始参数
- assign
coro_func
to cs.registers.rip
- 将当前标志复制到
cs.registers
(不是寄存器,只有标志,因为我们需要一些理智的标志来防止世界末日)
- 为协程的堆栈分配一些合适大小的区域并将其分配给
cs.registers.rsp
- 返回指向分配的指针
coroutine_state
结构
现在我们有另一个函数:
void* coroutine_next(coroutine_state cs, void* arg)
where cs
是从返回的结构coroutine_init
它代表一个协程实例,并且arg
当协程恢复执行时将被输入到协程中。
该函数由协程调用者调用,将一些新参数传递给协程并恢复它,该函数的返回值是协程返回(产生)的任意数据结构。
- 将所有当前标志/寄存器存储在
cs.caller_registers
除了RSP
,参见步骤 3。
- 存储
arg
in cs.arg
- 修复调用者堆栈指针(
cs.caller_registers.rsp
),添加2*sizeof(void*)
如果你幸运的话会修复它,你必须查找这个来确认它,你可能希望这个函数是stdcall,这样在调用它之前不会篡改寄存器
-
mov rax, [rsp]
, 分配RAX
to cs.caller_registers.rip
;解释:除非你的编译器是破解的,[RSP]
将保存指向调用该函数的调用指令后面的指令的指令指针(即:返回地址)
- 加载标志和寄存器
cs.registers
-
jmp cs.registers.rip
,有效地恢复协程的执行
请注意,我们永远不会从此函数返回,我们跳转到的协程会为我们“返回”(请参阅coroutine_yield
)。另请注意,在该函数内部,您可能会遇到许多复杂情况,例如 C 编译器生成的函数序言和结尾,也许还包括寄存器参数,您必须处理所有这些问题。就像我说的,stdcall 会拯救你lots麻烦的是,我认为 gcc -fomit-frame 指针会删除尾声的内容。
最后一个函数声明为:
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
。我们就说如果arg
is NULL
,则协程已终止,否则它是任意数据结构。
回顾一下,您可以使用以下命令初始化协程coroutine_init
,然后您可以重复调用实例化的协程coroutine_next
.
协程的函数本身声明为:void my_coro(coroutine_state cs)
cs.initarg
保存初始函数参数(想想构造函数)。每一次my_coro
叫做,cs.arg
有一个不同的参数,由coroutine_next
。这就是协程调用者与协程通信的方式。最后,每次协程想要暂停自己时,它都会调用coroutine_yield
,并向其传递一个参数,该参数是协程调用程序的返回值。
好吧,你现在可能会想“这很简单!”,但我忽略了以正确的顺序加载寄存器和标志的所有复杂性,同时仍然保持未损坏的堆栈帧并以某种方式保留协程数据结构的地址(你只需以线程安全的方式覆盖所有寄存器)。对于这一部分,您需要了解编译器内部是如何工作的...祝您好运:)