zne*_*eak 11 c assembly stack x86-64 coroutine
我决定尝试执行协同程序(我认为这就是我应该如何称呼它们)以获得乐趣和利润.我希望必须使用汇编程序,如果我想让它对任何事情都有用,可能还需要一些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
Run Code Online (Sandbox Code Playgroud)
我想要的是一种在不同堆栈上运行而无需线程化的两种函数的方法.(显然,一次只能运行一次.我说没有线程.)这两个函数必须能够恢复另一个执行(并暂停自己的执行).有点像他们正在longjmp对方.一旦它返回到另一个函数,它必须从它离开的地方恢复(即,在给另一个函数控制的调用期间或之后),有点像longjmp返回setjmp.
这就是我的想法:
A创建并将并行堆栈归零(分配内存和所有内容).A将其所有寄存器推送到当前堆栈.A将堆栈指针和基指针设置为该新位置,并推送一个神秘的数据结构,指示要跳回的位置以及将指令指针放回的位置.A大多数寄存器归零,并将指令指针设置为函数的开头B.这是初始化.现在,以下情况将无限循环:
B在该堆栈上运行,完成它需要的任何工作.B到了需要中断并A再次控制的程度.B将其所有寄存器推送到其堆栈,在最开始时采用神秘的数据结构 A,并将堆栈指针和指令指针设置为A告诉它的位置.在此过程中,它会提供A一个新的,经过修改的数据结构,告知其恢复的位置B.A唤醒,弹回它推送到堆栈的所有寄存器,并且一直工作直到它需要中断并B再次给出控制.这一切听起来都不错.但是,有很多事情我并不是很放心.
pusha条指令将所有寄存器发送到堆栈.然而,处理器架构不断发展,现在使用x86_64,我们有了更多的通用寄存器,可能还有几个SSE寄存器.我找不到任何pusha可以推动它们的证据.在现代的x86 CPU中有大约40个公共寄存器.我自己必须做所有的事push吗?此外,没有pushSSE寄存器(虽然必然是一个等价的 - 我对这整个"x86汇编程序"的新东西).mov rip, rax吗(英特尔语法)?此外,从中获取价值必须有点特殊,因为它会不断变化.如果我喜欢mov rax, rip(英特尔语法再次),将rip定位在mov指令上,指向它之后的指令,还是介于两者之间?jmp foo.假.pthread线程?感谢您阅读我的问题 textwall.
你是正确的,它PUSHA不会在x64上工作,它会引发异常#UD,因为PUSHA 只推动16位或32位通用寄存器.有关您想知道的所有信息,请参阅英特尔手册.
设置RIP很简单,jmp rax将设置RIP为RAX.要检索RIP,您可以在编译时获取它,如果您已经知道所有协同程序退出源,或者您可以在运行时获取它,则可以在该调用之后调用下一个地址.像这样:
a:
call b
b:
pop rax
Run Code Online (Sandbox Code Playgroud)
RAX现在将b.这是有效的,因为CALL推送下一条指令的地址.这种技术也适用于IA32(虽然我认为在x64上有更好的方法,因为它支持RIP相对寻址,但我不知道一个).当然,如果你创建一个函数coroutine_yield,它只能截取调用者地址:)
由于您不能在一条指令中将所有寄存器都压入堆栈,因此我不建议将协程状态存储在堆栈中,因为这会使事情变得复杂.我认为最好的做法是为每个协程实例分配一个数据结构.
你为什么把功能归零A?这可能没有必要.
以下是我将如何处理整个事情,尝试尽可能简单:
创建一个coroutine_state包含以下内容的结构:
initargargregisters (还包含标志)caller_registers创建一个功能:
coroutine_state* coroutine_init(void (*coro_func)(coroutine_state*), void* initarg);
where coro_func指向协同程序函数体的指针.
此功能执行以下操作:
coroutine_state结构csinitarg给cs.initarg,这些将是协程的初始参数coro_func给cs.registers.ripcs.registers(不是寄存器,只有标志,因为我们需要一些理智的标志来防止天启)cs.registers.rspcoroutine_state结构的指针现在我们有另一个功能:
void* coroutine_next(coroutine_state cs, void* arg)
cs返回的结构在哪里coroutine_init表示一个协程实例,并arg在恢复执行时被送入协同程序.
协同调用程序调用此函数将一些新参数传递给协程并恢复它,该函数的返回值是由协程返回(产生)的任意数据结构.
cs.caller_registers除外RSP,参见步骤3.arg在cs.argcs.caller_registers.rsp),添加2*sizeof(void*)将修复它,如果你很幸运,你必须仔细查看以确认它,你可能希望这个函数是stdcall所以没有寄存器被篡改之前调用它mov rax, [rsp],分配RAX给cs.caller_registers.rip; 解释:除非你的编译器处于破解状态,[RSP]否则会将指令指针保存到调用此函数的调用指令后面的指令(即:返回地址)cs.registersjmp cs.registers.rip,有效地恢复执行协程请注意,我们永远不会从这个函数返回,我们跳转到"返回"的协程(参见参考资料coroutine_yield).另请注意,在此函数中,您可能遇到许多复杂问题,例如由C编译器生成的函数序言和结尾,也许还有寄存器参数,您必须处理所有这些.就像我说的那样,stdcall会为你节省很多麻烦,我认为gcc的-fomit-frame_pointer会删除结尾的内容.
最后一个函数声明为:
void coroutine_yield(void* ret);
在协同程序内部调用此函数以"暂停"协程的执行并返回给调用者coroutine_next.
in cs.registerscs.registers.rsp),再次添加2*sizeof(void*)到它,并且您希望此函数也是stdcallmov rax, arg(让我们假装编译器中的所有函数都返回其参数RAX)cs.caller_registersjmp cs.caller_registers.rip这基本上是从coroutine_nextcoroutine调用者的堆栈帧上的调用返回的,并且由于返回值被传入RAX,我们返回arg.让我们说如果arg是NULL,那么协程已经终止,否则它是一个任意的数据结构.所以回顾一下,你初始化一个coroutine使用coroutine_init,然后你可以重复调用实例化的协同程序coroutine_next.
协程的函数本身被声明:
void my_coro(coroutine_state cs)
cs.initarg保存初始函数参数(想想构造函数).每次my_coro调用时,cs.arg都有一个由其指定的不同参数coroutine_next.这是协程调用者与协程通信的方式.最后,每当协同程序想要暂停时,它会调用coroutine_yield,并将一个参数传递给它,这是协同程序调用程序的返回值.
好吧,您现在可能会认为"那很简单!"但是我遗漏了以正确的顺序加载寄存器和标志的所有复杂情况,同时仍然保持一个未损坏的堆栈帧并以某种方式保留您的协程数据结构的地址(您只是以线程安全的方式覆盖所有寄存器.对于那部分,您将需要了解您的编译器如何在内部工作...祝你好运:)