# 人人都能学的会C++协程原理剖析与自我实现
- [人人都能学的会C++协程原理剖析与自我实现](#人人都能学的会c协程原理剖析与自我实现)
- [导语](#导语)
- [协程是什么?](#协程是什么)
- [协程的大致原理](#协程的大致原理)
- [C#下的yield return体验](#c下的yield-return体验)
- [C#下的分析与推导](#c下的分析与推导)
- [C++协程实现必用知识讲解](#c协程实现必用知识讲解)
- [函数的执行环境](#函数的执行环境)
- [函数的传参](#函数的传参)
- [函数的返回值](#函数的返回值)
- [函数的调用与返回](#函数的调用与返回)
- [理解Push ,Pop 指令的等效过程](#理解push-pop-指令的等效过程)
- [深入理解call,jmp,ret指令的等效过程](#深入理解calljmpret指令的等效过程)
- [获取EIP的值](#获取eip的值)
- [X86平台下的无独立栈协程](#x86平台下的无独立栈协程)
- [原理与C++代码的设计](#原理与c代码的设计)
- [谈__declspec(naked) ,__stdcall,__cdecl关键字](#谈__declspecnaked-__stdcall__cdecl关键字)
- [拆解协程函数的调用与重入设计](#拆解协程函数的调用与重入设计)
- [首调协程函数](#首调协程函数)
- [重入协程函数6步走](#重入协程函数6步走)
- [拆解协程函数保存上下文设计](#拆解协程函数保存上下文设计)
- [拆解协程函数退出设计](#拆解协程函数退出设计)
- [X64平台下基于boost库的fiber独立栈协程](#x64平台下基于boost库的fiber独立栈协程)
- [原理与C++代码的设计](#原理与c代码的设计-1)
- [拆解make_fcontext创建协程上下文的设计](#拆解make_fcontext创建协程上下文的设计)
- [拆解jump_fcontext切换协程上下文的设计](#拆解jump_fcontext切换协程上下文的设计)
## 导语
>本文将会深入讨论C/C++函数调用过程,执行过程,返回过程,这些是协程实现的基础,涉及部分汇编代码设计与分析,只有从汇编层次出发,才能揭示根本的原理,让我们不再停留表面。阅读本文之前先抛出一些相关问题,函数怎么提前返回?怎么手动模拟函数调用?函数参数到底怎么传?函数如何能跨调用层次返回?怎么能获取EIP/RIP?函数怎么跳转到特定地址执行?函数怎么保存上下文?汇编中为什么要保存寄存器?汇编中怎么恢复栈?汇编怎么平衡栈?什么是__stdcall和__cdecl?...如果这些问题你都很透彻了,肯定相信也能轻松理解协程,不用看这篇文章,也可以自己写出协程了。
本文是很久以前研究的,当时研究了boost库的协程,最近又翻了翻,发现都快忘光了,想想还是记录下,作为一篇笔记,如果能帮助别人,那再好不过了。本身boost的协程库跨平台,设计的也不错,结合C++模板,封装的比较深,易用难懂,想直接研究明白,有一定的门槛。想了想如果拿boost做教程,可能不够友好, 为了揭示协程原理,手写了两个简单实现DEMO,一个是Windows下X86无独立栈协程,一个是的提取boost的fiber汇编切换代码有独立栈协程。以前我也写了个clang跨平台的,处理不同平台也加了一些宏,复杂些,还不适合教程,所以为了做教程,周末我也花了一天多写了x86协程,写汇编切换部分相对费时间。我们掌握原理后,再看boost版本,就轻松易懂了,也能体会大佬设计的巧妙。
Demo工程代码:
[有栈与无栈协程DEMO](https://github.com/lishaojiang/talkcoroutine)
****
## 协程是什么?
协程不是线程,更不是进程,可以理解为当前线程中带上下文的子函数调用,好像概念比较抽象,我们再细化一些。对比线程来说,线程是由操作系统控制切换的,有独立上下文,协程切换是自己控制,也有独立上下文。
这些都是概念上的,不直观。从应用上说,比如释放一个技能,技能过程要有5s的动作动画播放后,再执行爆炸动画,整个过程是一个task。当然实现这个需求的方法有多种,我们做比较死的限定,要用类似C语言的顺程过程来执行,不要引入状态机或者其它技术段。那么执行这个task要等5s左右检查动作动画是否播放结束,这会阻塞住线程,效率很低,如果是带交互的程序,可能就被用户强关了。试想如果这个task放到新开工作线程中,好像也可以,但这就带来线程切换开销,如果这样的task很多,开了上千个线程,线程切换开销很大,就为检查一些特效是不是播放完,的确有点大材小用了,这时协程就派上场,可以理解为轻量级的小线程,当要等5s时,我直接退出,不阻塞,5s后我如果能再回到离开的状态,继续执行,模拟了线程,好像可以哇!协程也常用在网络与资源加载相关操作中。
## 协程的大致原理
上面小例子,实际有两点要求,一个函数离开时能记住离开的位置,在windows 平台下,也就是要能记住EIP/RIP寄存器值,因为EIP/RIP指示了下一条指令要执行的地址。一个是再次进入,能恢复现场,也要要能保存当时函数栈内存,寄存器的值,恢复栈内存,恢复寄存器的值,后面我们会详细说,也会手动完成这个过程,这要求我们对函数调用有深入的理解。
****
## C#下的yield return体验
C++20之前没有原生的协程,没有关系,C#有关键字有类似的功能,我们先看看C#的一段yield return示例代码,先结合运行结果,我们好进一步分析与推理,再过渡到C++层设计。
```C#
using System;
using System.Collections.Generic;
class Program
{
public static void Main(string[] args)
{
IEnumerable<int> Source(int max)
{
int first = 1, second = 1;
Console.WriteLine($"Source Show:{first} ");
yield return first;
Console.WriteLine($"Source Show:{second} ");
yield return second;
for (int i = 0; i < max; ++i)
{
int third = first + second;
first = second;
second = third;
Console.WriteLine($"Source Show:{third} ");
yield return third;
}
}
int iTimes = 0;
foreach (int i in Source(8))
{
Console.WriteLine($"Main Show:{i},Time:{++iTimes}");
}
Console.WriteLine();
}
}
```
看一下程序输出的结果:
![1](https://github.com/lishaojiang/talkcoroutine/blob/main/raw/1.png)
## C#下的分析与推导
从运行结果上看,明显Source的函数和foreach的执行语句交替执行,并且Source并不是一次全部执行完,每遇到yield return关键字就返回了,且下次调用时,还能接着从Source返回地方执行,有兴趣的可以打打断点,调试一下具体过程。
也就是说C#做了两件事,一件记录了函数退出时地址,下次调用可以直接到这个地址,一件是能能保存和恢复退出的现场,下次调用时可能恢复这个现场。是这个意思,只不过说的比较笼统,且这些底层都不是我们自己控制的,我们并太清楚编译器到底怎么实现的?我们下面转战到C++,来模拟这个过程,如果我们能模拟成功,说明我们真的理解了,并也提供了一种方案。
****
## C++协程实现必用知识讲解
### 函数的执行环境
函数的环境我的理解分为4个部分,分为可以执行的二进制代码,运行所需寄存器,运行所需栈内存,运行可能所需堆内存。
![2](https://github.com/lishaojiang/talkcoroutine/blob/main/raw/2.png)
* 二进制代码由编译器对生成