当前位置:   article > 正文

argobots && boost.fcontext(一)

argobots

GitHub
https://github.com/pmodels/argobots
官网
https://www.argobots.org/
官方给出的API说明
https://www.argobots.org/

官方定义
Argobots is a lightweight, low-level threading and tasking framework.
argobots是一款轻量级的线程和任务框架。
人话
协程库和接口

什么是协程

简单介绍协程,协程就是用户态的线程,线程由用户态的程序创建,但其调度策略还是由操作系统负责,同时线程过多的情况下操作系统切换线程本身的消耗也很大,而协程的调度由用户自己决定,两个协程间的切换也由用户自己决定,所以极大的减少了切换的损耗,同时协程有利于减少锁的开销,因为一组协程都是由一个线程去执行的,两个协程间的并发实际上是串行的(前提是两个协程同隶属于一个线程去执行),所以能减少锁的使用,在某种程度上?

从上述角度来理解协程的话,一个协程库其最主要的就是如何实现重入,即一个函数在运行到一半切走后,再度调度到时能保证函数能够继续按照用户的期望执行下去,线程或进程在切换时由于其当时的内存状态操作系统会保留,所以不需要程序员自己去控制,当然也有很多接口让程序员可以指定某些线程的调度模式和调度优先级这些,而协程的切换并不由操作系统控制,至少就目前来看,所以参照线程的切换,如何保存协程的上下文便是协程实现的核心。

协程分为有栈和无栈两种类型,stackful和stackless,无栈协程的常见实现依赖于C语言switch的一个机制–Duff’s Device,这部分代码不是很复杂,boost库最开始的协程库coroutine实现也是基于此。这里主要看stackful协程,这部分的实现底层由汇编完成,由boost fcontext和ucontext提供,本文提到的argobots,和golang中的goroutine基本都是复用了这一段汇编代码。这里提到的汇编实现只提供了协程栈的创建,协程的切换等比较基础的功能,针对另外一个比较关键的点,协程的调度,不同库的实现差异很大,需要单独研究。

函数的执行相关

要理解stackful协程,就需要理解函数的调用和执行的过程。

不论是什么代码,最后都会被编译成一个可执行的二进制文件,二进制文件被加载到内存里后,cpu会从这些二进制文件里读取一个个汇编指令(当然其实是机器指令,这些机器指令程序员可以通过objdump看到他的汇编指令),然后做出相应的行为,从而完成整个程序的运行

关于二进程文件被加载到的所谓“内存”,这里只讨论虚拟内存,暂不关心其他的映射,操作系统会给一个内存分配一段逻辑上连续的内存空间,也就是常见的下图
在这里插入图片描述
图示是一个32位的linux内核运行的程序的内存布局图,当然也是虚拟内存,现在大部分服务器都是64位的,但总体上的布局32位的差不太多,至少看起来差不太多。每段介绍如下

文本段(Text):也称为代码段。进程启动时会将程序的代码加载到物理内存中,文本段映射到这片物理内存。
初始化数据(Data):包含程序显式初始化的全局变量和静态变量,这些数据是在程序真正运行前就已经确定的数据,所以可以提前加载到内存保存好。
未初始化数据(BSS):未初始化的全局变量和静态变量,这些变量的值是在程序真正运行起来并为其赋值后才能确定的,所以程序加载之初,只需要记录它的内存地址和所需大小。出于历史原因,这段空间也称为BSS段。
栈(Stack):是一个可以动态增长和收缩的内存段落,由栈帧(Stack Frames)组成,进程每调用一次函数,都将为该函数分配一个栈帧,栈帧中保存了该函数的局部变量、参数值和返回值。注意,编译器会将函数参数放入寄存器来优化程序,只有寄存器放不下的参数才使用栈帧来保存。
堆(Heap):程序运行时,变量的值以及动态请求(malloc, new等)分配的内存都在这个内存段落中。
内核段(Kernel):这部分是操作系统内核运行时所占用内存在各进程虚拟地址空间中的映射。所有进程都有,且映射地址相同,因为都映射到内核使用的内存。这段内存只有内核能访问,用户进程无法访问到该段落。

这里重点关注两个区段,Stack 和 Text 段,因为需要关注的所谓协程,本质上还是函数一个个一层层调用执行的过程,从介绍里可以知道函数运行的代码(即指令)是保存在Text段的,而函数运行过程中的一些变量,参数,返回值(即通常所说的函数上下文),是保存在栈(Stack)空间内的。

通过一个例子更直观的感受下

//文件名aaa.c
#include <stdint.h>
int plus(int a, int b) {
    return a + b;
}

int main()
{
    int x;
    x = plus(1, 2);
    sleep(3 * 1000);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这是一个非常简单的C语言程序,在main函数退出前加了一个长时间的sleep方便我们观察

gcc -g aaa.c -o aaa
./aaa
  • 1
  • 2

函数运行起来后会一直在sleep的地方等待,先查一下aaa跑起来的进程id然后通过命令观察下他的内存布局,这里用的是64位的CentOS8,所以

[root@node1 demo]# ps aux | grep aaa
root       69869  0.0  0.0   4236   876 pts/1    S+   21:33   0:00 ./aaa
root       69871  0.0  0.0  12136  1156 pts/5    R+   21:33   0:00 grep --color=auto aaa
[root@node1 demo]# cat /proc/69869/maps 
00400000-00401000 r-xp 00000000 08:00 385037                             /home/daos/abt_demo/aaa
00600000-00601000 r--p 00000000 08:00 385037                             /home/daos/abt_demo/aaa
00601000-00602000 rw-p 00001000 08:00 385037                             /home/daos/abt_demo/aaa
7f1ee9566000-7f1ee9722000 r-xp 00000000 103:03 180414                    /usr/lib64/libc-2.28.so
7f1ee9722000-7f1ee9921000 ---p 001bc000 103:03 180414                    /usr/lib64/libc-2.28.so
7f1ee9921000-7f1ee9925000 r--p 001bb000 103:03 180414                    /usr/lib64/libc-2.28.so
7f1ee9925000-7f1ee9927000 rw-p 001bf000 103:03 180414                    /usr/lib64/libc-2.28.so
7f1ee9927000-7f1ee992b000 rw-p 00000000 00:00 0 
7f1ee992b000-7f1ee9957000 r-xp 00000000 103:03 180407                    /usr/lib64/ld-2.28.so
7f1ee9b3f000-7f1ee9b42000 rw-p 00000000 00:00 0 
7f1ee9b55000-7f1ee9b57000 rw-p 00000000 00:00 0 
7f1ee9b57000-7f1ee9b58000 r--p 0002c000 103:03 180407                    /usr/lib64/ld-2.28.so
7f1ee9b58000-7f1ee9b5a000 rw-p 0002d000 103:03 180407                    /usr/lib64/ld-2.28.so
7ffe4d7ff000-7ffe4d820000 rw-p 00000000 00:00 0                          [stack]
7ffe4d9b8000-7ffe4d9bc000 r--p 00000000 00:00 0                          [vvar]
7ffe4d9bc000-7ffe4d9be000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

根据回显可以很明显的看到,高地址的0x7ffe4d9b8000 - 0xffffffffff601000也就是cat命令回显的最后三行,都是用于系统调用和一些内核相关的数据存放,然后是一段栈(stack)地址空间,接下来就是一些链接的库的加载,因为程序比较简单所以这里只链接了标准C库和专门用于装载动态库的库ld.so。然后头三行也就是低地址的三行显示的是直接映射了我们编译出的可执行文件aaa上,这就是用于存放代码也就是函数运行的一个个指令区段。

然后就是观察函数运行的实际,已知Text段会存放运行这个程序的指令,也就是我们编译出来的二进制文件,所以直接通过 objdump 把编译好的可执行文件的汇编打出来。

//虽然代码很简单,但是编译成汇编涉及到库的链接还是很多,这里只截取一部分反编译得到的汇编代码
[root@node1 demo]# objdump -d aaa 

Disassembly of section .text:

00000000004004a0 <_start>:
  4004a0:	f3 0f 1e fa          	endbr64 
  4004a4:	31 ed                	xor    %ebp,%ebp
  4004a6:	49 89 d1             	mov    %rdx,%r9
  4004a9:	5e                   	pop    %rsi
  4004aa:	48 89 e2             	mov    %rsp,%rdx
  4004ad:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
  4004b1:	50                   	push   %rax
  4004b2:	54                   	push   %rsp
  4004b3:	49 c7 c0 40 06 40 00 	mov    $0x400640,%r8
  4004ba:	48 c7 c1 d0 05 40 00 	mov    $0x4005d0,%rcx
  4004c1:	48 c7 c7 9a 05 40 00 	mov    $0x40059a,%rdi
  4004c8:	ff 15 1a 0b 20 00    	callq  *0x200b1a(%rip)        # 600fe8 <__libc_start_main@GLIBC_2.2.5>
  4004ce:	f4                   	hlt    

0000000000400586 <plus>:
  400586:	55                   	push   %rbp
  400587:	48 89 e5             	mov    %rsp,%rbp
  40058a:	89 7d fc             	mov    %edi,-0x4(%rbp)
  40058d:	89 75 f8             	mov    %esi,-0x8(%rbp)
  400590:	8b 55 fc             	mov    -0x4(%rbp),%edx
  400593:	8b 45 f8             	mov    -0x8(%rbp),%eax
  400596:	01 d0                	add    %edx,%eax
  400598:	5d                   	pop    %rbp
  400599:	c3                   	retq   

000000000040059a <main>:
  40059a:	55                   	push   %rbp
  40059b:	48 89 e5             	mov    %rsp,%rbp
  40059e:	48 83 ec 10          	sub    $0x10,%rsp
  4005a2:	be 02 00 00 00       	mov    $0x2,%esi
  4005a7:	bf 01 00 00 00       	mov    $0x1,%edi
  4005ac:	e8 d5 ff ff ff       	callq  400586 <plus>
  4005b1:	89 45 fc             	mov    %eax,-0x4(%rbp)
  4005b4:	bf b8 0b 00 00       	mov    $0xbb8,%edi
  4005b9:	b8 00 00 00 00       	mov    $0x0,%eax
  4005be:	e8 cd fe ff ff       	callq  400490 <sleep@plt>
  4005c3:	b8 00 00 00 00       	mov    $0x0,%eax
  4005c8:	c9                   	leaveq 
  4005c9:	c3                   	retq   
  4005ca:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

暂时没有汇编基础也关系不大,这里看到的函数有三个,有两个是代码中的函数main和plus编译成汇编后的结果,start函数是运行时需要的,这个是glibc的函数,他的作用就是去调用main函数,可以很明显看到start函数中有一行

4004c1:	48 c7 c7 9a 05 40 00 	mov    $0x40059a,%rdi
//PS:这里多提一句,如果你跟本人一样,是个正常人,开始也不懂汇编,
//要么是看小甲鱼的课或者看王爽的汇编语言走但又不太认真的,要注意下
//汇编有两种语法一种叫intel语法,一个跟我一样的正常人,一般初学汇
//编看到的代码都是这种语法,比如mov指令,按照intel语法的语义是 
//mov dest, src。另外一种是AT&T语法,他的语义刚好是反过来的,是
// mov src, dest。这两种语法的区分方式最鲜明的就是寄存器有无%,
//立即数有无$,比如
mov $0x42 %ah //AT&T语法,表示把立即数0x42放到ah寄存器中
mov ah 0x42   //intel语法,效果相同
//基本上现在64位linux服务器的汇编都是采用的AT&T语法了,所以要注意区分下。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这行的src是个立即数0x40059a,目标是个寄存器rdi,然后通过call调用 *0x200b1a(%rip) 这个值,这里先不解释这些寄存器的作用,但是这个操作就可以成功调用到代码中的main函数了,然后main函数执行过程中又调用了plus函数。而在这些函数前看到的这些0x40059a 0x400586其实都是程序在实际运行过程这些函数中被加载到虚拟内存后的地址。结合上图看到的内存布局图,把程序跑起来通过gdb实际验证一下。

Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-164.el8.x86_64
(gdb)  x/32xb 0x400586
0x400586 <plus>:	0x55	0x48	0x89	0xe5	0x89	0x7d	0xfc	0x89
0x40058e <plus+8>:	0x75	0xf8	0x8b	0x55	0xfc	0x8b	0x45	0xf8
0x400596 <plus+16>:	0x01	0xd0	0x5d	0xc3	0x55	0x48	0x89	0xe5
0x40059e <main+4>:	0x48	0x83	0xec	0x10	0xbe	0x02	0x00	0x00
(gdb)  x/32xb 0x40059a
0x40059a <main>:	0x55	0x48	0x89	0xe5	0x48	0x83	0xec	0x10
0x4005a2 <main+8>:	0xbe	0x02	0x00	0x00	0x00	0xbf	0x01	0x00
0x4005aa <main+16>:	0x00	0x00	0xe8	0xd5	0xff	0xff	0xff	0x89
0x4005b2 <main+24>:	0x45	0xfc	0xbf	0xb8	0x0b	0x00	0x00	0xb8
(gdb)  x/64xb 0x40059a
0x40059a <main>:	0x55	0x48	0x89	0xe5	0x48	0x83	0xec	0x10
0x4005a2 <main+8>:	0xbe	0x02	0x00	0x00	0x00	0xbf	0x01	0x00
0x4005aa <main+16>:	0x00	0x00	0xe8	0xd5	0xff	0xff	0xff	0x89
0x4005b2 <main+24>:	0x45	0xfc	0xbf	0xb8	0x0b	0x00	0x00	0xb8
0x4005ba <main+32>:	0x00	0x00	0x00	0x00	0xe8	0xcd	0xfe	0xff
0x4005c2 <main+40>:	0xff	0xb8	0x00	0x00	0x00	0x00	0xc9	0xc3
0x4005ca:	0x66	0x0f	0x1f	0x44	0x00	0x00	0xf3	0x0f
0x4005d2 <__libc_csu_init+2>:	0x1e	0xfa	0x41	0x57	0x49	0x89	0xd7	0x41
(gdb) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

0x400586 和 0x40059a 分别是刚才在汇编代码中看到的plus函数的地址和main函数的地址,现在在一个实际运行中的程序的内存布局里通过直接打印内存的方式也可以看到这些值,而且看起来gdb能够直接识别这些汇编代码,不过通过16进制值的对比也能够确认。

到这里就大概能够清楚一个CPU和程序虚拟内存之间的关系了,但是刚才在汇编代码中也能够看到,汇编指令很多时候看起来并不像是CPU会去直接操作内存单元,而是操作各种类似于rdi rax 这样的东西,这个东西就是寄存器,网上看到过一个很形象的例子,寄存器就是太监,内存就是大臣,皇上就是CPU,大臣所有的奏折都是太监递给皇上的,皇上颁布的诏令也是通过太监传下去的,之所以这么做就是皇上要是没发一个诏令就跑一趟太累了,而让太监去做招招手就行,类比到寄存器就是虽然我们一般知道内存的读写速度很快,但和寄存器比起来还是慢太多了,所以有些数据存在寄存器里能够大幅度提升程序运行的效率。而汇编代码看到的rdi rdx rax这些都是寄存器,因为CPU大小有限,所以寄存器的个数也是有限的。

按照intel官方给出的文档,x86_64这个架构下的CPU寄存器一共有几十个(官方文档英文的,没细看完,照着目录数了下章节估了个大概值,50多个的样子?),当然一般做应用程序员主要关心的还是通用寄存器,x86_64的通用寄存器的介绍网上很多,很多汇编的书籍课程有很详细的介绍,这里不赘述了,这些通用寄存的效果和用途也没有发生啥大变化,而且兼容了32位的寄存器。

boost.fcontext

了解了函数的运行过程后,就要看下协程在干嘛了,关于boost fcontext库,他提供了多种操作系统的实现源码。这里取服务器较为常用的x86_64 linux下的实现,主要就三个文件,都是汇编代码
take_x86_64_sysv_elf_gas.S
jump_x86_64_sysv_elf_gas.S
make_x86_64_sysv_elf_gas.S
从代码的命令上可以比较直观的感受到,之前提到的协程的实现关键在于用户态的上下文切换,这里可以猜测make负责创建协程栈,jump负责协程间的跳转,take表示获取上下午,一般在结束的时候用,然后依次看一下这三个功能的实现

make_fcontext

从顺序来看,首先肯定是创建,也就是make,首先按照boost官方给出的文档查下make_fcontext的用法

typedef void* fcontext_t;
fcontext_t BOOST_CONTEXT_CALLDECL make_fcontext( void * sp, std::size_t size, void (* fn)( intptr_t) );
  • 1
  • 2

调用这个函数时会给三个参数,第一个void*指向一块已经分配过的空间地址,第二个size_t参数表示这个栈的大小,这个值其实就是给第一个参数sp分配的指针大小,第三个是函数参数,表示要执行的函数。
因为有三个参数,所以按照intel x86_84的默认规则,调用make_fcontext时这三个参数会由调用函数依次放在rdi,rsi和rdx寄存器中。然后call make_fcontext,make_fcontext源码如下(linux x86_64版本)

/*
            Copyright Oliver Kowalke 2009.
   Distributed under the Boost Software License, Version 1.0.
      (See accompanying file LICENSE_1_0.txt or copy at
            http://www.boost.org/LICENSE_1_0.txt)
*/

/****************************************************************************************
 *                                                                                      *
 *  ----------------------------------------------------------------------------------  *
 *  |    0    |    1    |    2    |    3    |    4     |    5    |    6    |    7    |  *
 *  ----------------------------------------------------------------------------------  *
 *  |   0x0   |   0x4   |   0x8   |   0xc   |   0x10   |   0x14  |   0x18  |   0x1c  |  *
 *  ----------------------------------------------------------------------------------  *
 *  | fc_mxcsr|fc_x87_cw|        R12        |         R13        |        R14        |  *
 *  ----------------------------------------------------------------------------------  *
 *  ----------------------------------------------------------------------------------  *
 *  |    8    |    9    |   10    |   11    |    12    |    13   |    14   |    15   |  *
 *  ----------------------------------------------------------------------------------  *
 *  |   0x20  |   0x24  |   0x28  |  0x2c   |   0x30   |   0x34  |   0x38  |   0x3c  |  *
 *  ----------------------------------------------------------------------------------  *
 *  |        R15        |        RBX        |         RBP        |        RIP        |  *
 *  ----------------------------------------------------------------------------------  *
 *  ----------------------------------------------------------------------------------  *
 *  |    16   |   17    |                                                            |  *
 *  ----------------------------------------------------------------------------------  *
 *  |   0x40  |   0x44  |                                                            |  *
 *  ----------------------------------------------------------------------------------  *
 *  |        EXIT       |                                                            |  *
 *  ----------------------------------------------------------------------------------  *
 *                                                                                      *
这张图是调用给出的调用make_fcontext后的内存布局图 ****************************************************************************************/

.text
.globl make_fcontext
.type make_fcontext,@function
.align 16
make_fcontext:
    /* first arg of make_fcontext() == top of context-stack */
    /*第一个参数void* sp在调用前存放于rdi寄存器中,这里将该值放到rax寄存器中*/
    movq  %rdi, %rax

    /* shift address in RAX to lower 16 byte boundary */
    /*将rax寄存器中的值和立即数-16做与操作,因为是andq,所以-16最后16进制展示会变成
      0xfffffffffffffff0,这里的目的是调整栈指针地址,rax寄存器中放的值是void* sp
      将这个值和-16做与相当于将该指针的低四位取零*/
    andq  $-16, %rax

    /* reserve space for context-data on context-stack */
    /* size for fc_mxcsr .. RIP + return-address for context-function */
    /* on context-function entry: (RSP -0x8) % 16 == 0 */
    /* 将rax寄存器中的值减上0x48,结果还是放在rax寄存器中*/
    leaq  -0x48(%rax), %rax

    /* third arg of make_fcontext() == address of context-function */
    /*将调用该函数传入的第三个参数,也就是fn函数的函数指针,放在“rax寄存器所指向内存地址+0x38”这个内存地址中*/
    movq  %rdx, 0x38(%rax)

    /* save MMX control- and status-word */
    /*下面两个指令是针对intel系列处理器特有的浮点相关的寄存器指令,intel有专门针对浮点计算的寄存器堆栈,这里暂不深入,只了解这两个指令就是将浮点寄存器的相关值放入了rax寄存器所指向的内存地址*/
    stmxcsr  (%rax)
    /* save x87 control-word */
    fnstcw   0x4(%rax)

    /* compute abs address of label finish */
    /*根据上面的英文注释,这一行是计算label finish,也就是汇编代码中的finish函数的绝对地址,并将结果放入到rcx寄存器种,这里需要注意这里定义的“绝对地址”,这里finish是相对地址,取得是finish函数到本行汇编代码的距离,加上rip寄存器里的值,rip会指向正在执行的汇编代码的地址,所以两者相加就是finish函数的绝对地址*/
    leaq  finish(%rip), %rcx
    /* save address of finish as return-address for context-function */
    /* will be entered after context-function returns */
    /*将rcx寄存器里的值,也就是刚刚获得到的finish函数的地址,放到内存地址为“rax寄存器中所存放的地址+0x40”处*/
    movq  %rcx, 0x40(%rax)
    /*返回*/
    ret /* return pointer to context-data */

finish:
    /* exit code is zero */
    xorq  %rdi, %rdi
    /* exit application */
    call  _exit@PLT
    hlt
.size make_fcontext,.-make_fcontext

/* Mark that we don't need executable stack. */
#ifndef __SUNPRO_C
.section .note.GNU-stack,"",%progbits
#else
/* Solaris assembler does not recognize it.  Let's ignore so far. */
#endif                                    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

因为汇编调用一个函数的返回值会默认放在rax寄存器中,所以这样执行过make_fcontext函数后,我们会得到一个地址,而从代码来看,rax寄存器中的值会有如下变化
1.第一次是将rdi寄存器的值直接传入rax寄存器
2.将rax寄存器中的值和-16(补码后是0xfffffffffffffff0)做与运算
3.再次将rax寄存器内部的值向低位偏移0x48字节,也就是72字节的空间
也就是说我们给make_fcontext函数传入的第一个参数,经过上述三步后获得一个新的地址,并且能获得从该地址其实一段长为72+size字节长度的空间(72是通过lea命令取得,加上本身指针malloc的空间),并且前72个字节的空间的内存分布如代码注释所示。

通过一个简单的代码+gdb可以看到make_fcontext的运行流程

//文件名make_fcontext.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
typedef void *fcontext_t;

#define ABT_API_PRIVATE

fcontext_t make_fcontext(void *sp, size_t size,
                         void (*thread_func)(void *)) ABT_API_PRIVATE;
void *jump_fcontext(fcontext_t *old, fcontext_t new, void *arg) ABT_API_PRIVATE;
void *take_fcontext(fcontext_t *old, fcontext_t new, void *arg) ABT_API_PRIVATE;
void init_and_call_fcontext(void *p_arg, void (*f_thread)(void *),
                            void *p_stacktop, fcontext_t *old);

void peek_fcontext(fcontext_t new, void (*peek_func)(void *),
                   void *arg) ABT_API_PRIVATE;

void hello_world(void *args) {
    printf("hello world\n");
}

int main()
{
    size_t st_size = 16384;
    void* ctx = malloc(st_size);
    printf("ctx: %p   %p\n", ctx, hello_world);
    void* a = make_fcontext(ctx, st_size, hello_world);
    printf("fcontext_t: %p\n", a);
    sleep(3 * 1000);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

编译后通过gdb启动,将断点设置在make_fcontext处,打开layout regs查看寄存器变化

[root@node1 fcontext]# ls
jump_x86_64_sysv_elf_gas.S  make_fcontext.c  make_x86_64_sysv_elf_gas.S  take_x86_64_sysv_elf_gas.S
[root@daos fcontext]# gcc -g -o make_fcontext make_fcontext.c jump_x86_64_sysv_elf_gas.S make_x86_64_sysv_elf_gas.S take_x86_64_sysv_elf_gas.S 
make_fcontext.c: In function ‘main’:
make_fcontext.c:29:2: warning: implicit declaration of function ‘sleep’ [-Wimplicit-function-declaration]
  sleep(3 * 1000);
  ^~~~~
[root@node1 fcontext]# gdb ./make_fcontext
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这里插入图片描述
这时候函数断在make_fcontext函数入口处,之前的操作可以编译后的二进制文件可以通过objdump查看汇编代码,在函数进入make_fcontext前已经将三个参数从左到右依次放入rdi,rsi,rdx寄存器,进入make_fcontext函数后,依次经过几条指令将我们分配的指针0x6022a0放入rax并最终变成了0x602258。
这时候通过打印0x602258开头的内存空间可以看到预留出来的72字节还是空的
执行完mov指令后能看到rdx寄存器中的值,也是我们hello_world函数的函数指针,被放到了距离0x602258地址0x38距离的内存中
两个intel专门针对浮点寄存器的操作后,浮点寄存器的状态被放到了0x602258指向空间的开头
经过lea以及最后一个mov指令后,rcx寄存器的值变成了finish函数的地址0x40081,并且被放入了距离0x602258所指向地址再加64字节的地址中
这样就创建好了一个协程栈。接下来就是如何实现跳转,用到的就是jump_fcontext函数

jump_fcontext

先看下代码实现(x86_64 linux)

/*
            Copyright Oliver Kowalke 2009.
   Distributed under the Boost Software License, Version 1.0.
      (See accompanying file LICENSE_1_0.txt or copy at
            http://www.boost.org/LICENSE_1_0.txt)
*/

/****************************************************************************************
 *                                                                                      *
 *  ----------------------------------------------------------------------------------  *
 *  |    0    |    1    |    2    |    3    |    4     |    5    |    6    |    7    |  *
 *  ----------------------------------------------------------------------------------  *
 *  |   0x0   |   0x4   |   0x8   |   0xc   |   0x10   |   0x14  |   0x18  |   0x1c  |  *
 *  ----------------------------------------------------------------------------------  *
 *  | fc_mxcsr|fc_x87_cw|        R12        |         R13        |        R14        |  *
 *  ----------------------------------------------------------------------------------  *
 *  ----------------------------------------------------------------------------------  *
 *  |    8    |    9    |   10    |   11    |    12    |    13   |    14   |    15   |  *
 *  ----------------------------------------------------------------------------------  *
 *  |   0x20  |   0x24  |   0x28  |  0x2c   |   0x30   |   0x34  |   0x38  |   0x3c  |  *
 *  ----------------------------------------------------------------------------------  *
 *  |        R15        |        RBX        |         RBP        |        RIP        |  *
 *  ----------------------------------------------------------------------------------  *
 *  ----------------------------------------------------------------------------------  *
 *  |    16   |   17    |                                                            |  *
 *  ----------------------------------------------------------------------------------  *
 *  |   0x40  |   0x44  |                                                            |  *
 *  ----------------------------------------------------------------------------------  *
 *  |        EXIT       |                                                            |  *
 *  ----------------------------------------------------------------------------------  *
 *                                                                                      *
jump_fcontext(&src_context, dst_context, void* arg)
这里的src_context dst_context均为通过make_fonctext得到的指针,arg为需要传递给dst_context的参数
****************************************************************************************/
.text
.globl jump_fcontext
.type jump_fcontext,@function
.align 16
jump_fcontext:
	/*这里是函数调用的常规操作,因为这六个寄存器按照intel的文档都是callee-saved,被调用的函数需要自己保存这六个寄存器的值,于是这里将这六个寄存器入栈*/
    pushq  %rbp  /* save RBP */
    pushq  %rbx  /* save RBX */
    pushq  %r15  /* save R15 */
    pushq  %r14  /* save R14 */
    pushq  %r13  /* save R13 */
    pushq  %r12  /* save R12 */

    /* prepare stack for FPU */
    /*intel专门的浮点寄存器的值,sp指针下移为这个寄存器的值留出空间*/
    leaq  -0x8(%rsp), %rsp

#if ABTD_FCONTEXT_PRESERVE_FPU
	/*同上,如果需要保存浮点寄存器状态,则放入刚刚保留的空间中*/
    /* save MMX control- and status-word */
    stmxcsr  (%rsp)
    /* save x87 control-word */
    fnstcw   0x4(%rsp)
#endif
	/*修改rsp 此时已经改变到其他栈
	 *将rsp 保存到第一个参数指向的内存中。fcontext_t *ofc */
    /* store RSP (pointing to context-data) in RDI */
    movq  %rsp, (%rdi)

	/*实现了将第二个参数复制到 rsp.*/
    /* restore RSP (pointing to context-data) from RSI */
    movq  %rsi, %rsp

#if ABTD_FCONTEXT_PRESERVE_FPU
	/*恢复浮点寄存器相关*/
    /* restore MMX control- and status-word */
    ldmxcsr  (%rsp)
    /* restore x87 control-word */
    fldcw  0x4(%rsp)
#endif
	/*按顺序将之前保存到栈中数据重新加载到寄存器*/
    /* prepare stack for FPU */
    leaq  0x8(%rsp), %rsp

    popq  %r12  /* restrore R12 */
    popq  %r13  /* restrore R13 */
    popq  %r14  /* restrore R14 */
    popq  %r15  /* restrore R15 */
    popq  %rbx  /* restrore RBX */
    popq  %rbp  /* restrore RBP */

    /* restore return-address */
    popq  %r8

    /* use third arg as return-value after jump */
    /* movq  %rdx, %rax */
    /* use third arg as first arg in context function */
    movq  %rdx, %rdi
	
	/*最终跳转到新函数*/
    /* indirect jump to context */
    jmp  *%r8
.size jump_fcontext,.-jump_fcontext
/*init_and_call_fcontext和peek_context不是协程“切换”的必要步骤,暂时先不研究*/
#if ABT_CONFIG_THREAD_TYPE == ABT_THREAD_TYPE_DYNAMIC_PROMOTION
.text
.globl init_and_call_fcontext
.type init_and_call_fcontext,@function
.align 16
init_and_call_fcontext:
    /* save the current rsp to the new stack,
     * which will be restored by movq 0x8(%rsp), %rsp */
    movq %rsp, -0x8(%rdx)
    /* save callee-saved registers */
    pushq %rbp  /* save RBP */
    pushq %rbx  /* save RBX */
    pushq %r15  /* save R15 */
    pushq %r14  /* save R14 */
    pushq %r13  /* save R13 */
    pushq %r12  /* save R12 */
    /* prepare stack for FPU */
    leaq  -0x8(%rsp), %rsp
#if ABTD_FCONTEXT_PRESERVE_FPU
    /* save MMX control- and status-word */
    stmxcsr  (%rsp)
    /* save x87 control-word */
    fnstcw   0x4(%rsp)
#endif
    /* store RSP in RCX (= fctx) */
    movq %rsp, (%rcx)
    leaq -0x10(%rdx), %rsp
    /* call RSI (= f_thread). RDI (= p_arg) has been already set */
    /* RSP is 16-byte aligned (ABI specification) */
    callq *%rsi
    /* restore original RSP */
    movq 0x8(%rsp), %rsp
    /*
     * - When the thread did not yield, RSP is set to the original one, so ret
     *   jumps to the original control flow.
     * - Any suspension updates RSP to (p_stack - 0x10), so that ret
     *   calls (p_stack - 0x8), which is set to a termination function.
     *   RSP is 16-byte aligned (ABI specification).
     */
    ret
.size init_and_call_fcontext,.-init_and_call_fcontext
#endif

.text
.globl peek_fcontext
.type peek_fcontext,@function
.align 16
peek_fcontext:
    /* temporarily move RSP (pointing to context-data) to R12 (callee-saved) */
    pushq  %r12
    movq   %rsp, %r12
    /* restore RSP (pointing to context-data) from RDI */
    movq   %rdi, %rsp
    /* RSP is already 16-byte aligned, so we can call a peek function here
     * rsi(rdx) => second_arg(third_arg) */
    movq   %rdx, %rdi
    callq *%rsi
    /* restore callee-saved registers. */
    movq   %r12, %rsp
    popq   %r12
    ret
.size peek_fcontext,.-peek_fcontext

/* Mark that we don't need executable stack.  */
#ifndef __SUNPRO_C
.section .note.GNU-stack,"",%progbits
#else
/* Solaris assembler does not recognize it.  Let's ignore so far. */
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/309004
推荐阅读
相关标签
  

闽ICP备14008679号