Libco协程切换的原理

函数调用的原理

linux 程序内存布局

传统linux程序(32bit)拥有4G的虚拟内存区域,高1G的区域供内核使用,剩余的3G内存供程序使用。按段划分,主要分程序段(text segement)、数据段、BSS段。BSS段用于未初始化的静态变量的初始化(0值初始化)。栈从高到低地址增长。堆从低到高增长。栈和堆的这两种不同的地址增长方向,需要关注下,后面协程切换中就涉及到该布局的
不同。
在这里插入图片描述

栈帧定义与函数调用
两个寄存器
  • esp 栈顶指针寄存器,指向调用栈的栈顶(始终指向,意味着栈分配到哪里了,从当前栈往高地址是已被分配了的)
  • ebp 基址指针寄存器,指向当前活动栈帧的基址
其他:

X86-64有16个64位寄存器,分别是:

%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。

其中:

  • %rax 作为函数返回值使用。
  • %rsp 栈指针寄存器,指向栈顶
  • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
  • %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
  • %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
栈帧
  • 栈帧是从栈上分配的一段内存,每次函数调用时,用于存储自动变量。从物理介质角度看,栈帧是位于esp(栈指针)及ebp(基指针)之间的一块区域。局部变量等分配均在栈帧上分配,函数结束自动释放。

这里我们看一下一个栈帧在内存中的布局:

在这里插入图片描述

  • 注意在这里相当于是一个倒栈

红色区域是进入新的函数中时,栈帧的内存布局。这里需要关注红色区域之上的参数2,参数1,和返回地址。即每次进行函数调用时,会将参数从右到左的入栈,最后将返回地址入栈。这个返回地址是指,函数调用结束后的下一行执行的代码地址获取参数及返回地址通过EBP相对偏移即可获得,图示为32位系统对应的偏移值。

x86_64架构下gnu编译器c/cpp函数调用解析:

首先需要确定的是:

每一次函数调用都是一个栈帧
函数调用流程:
  • 1 传参,主要是传递给寄存器。当寄存器不够用时,会丛右到左压栈,然后再传参给寄存器( %rdi,%rsi,%rdx,%rcx,%r8,%r9,这6个不够用的时候才会借用栈。所以rdi是第一个参数,是保存寄存器组内存的首地址)
  • 2 将返回地址压栈,该地址一般指向函数返回后的下一条指令
  • 3 修改rip寄存器(指令寄存器)为调用函数的起始地址,新的函数开始了
  • 4 将上个函数的栈帧基址(rbp寄存器用于存放栈帧基址)压入栈中
  • 5 将rbp寄存器中的值修改为rsp寄存中的值,即开启了新的栈帧
  • 6 在栈里为当前函数局部变量分配所需的空间,表现为修改ESP寄存器的值
  • 7 函数返回时,通过 rbp 的偏移就能够获得原先的参数和返回地址

OK,知道了函数调用的原理,接下来我们来实际的看一看函数的调用过程:使用 GDB
测试程序:

#include<stdio.h>
int func(int a,int b){
    int c= 0 ;
    int d= 1;
    c=  a*b; 
    printf("c is %d \n",c);

}

int main(void)
{
    func(1,2); //gdb 在这里停下来
    return 0;
}

具体见:https://blog.youkuaiyun.com/weijitao/article/details/46794573

disass  //展示汇编代码
i  reg      // 查看寄存器

在这里插入图片描述

coctx_swap.S 汇编解读(只关注__x86_64__)

在这个汇编文件定义了函数 coctx_swap 函数(co_routine->coctx_swap())。该函数的作用是 保存当前 co_routine 的执行环境到结构体 coctx_t ,然后将CPU上下文设置为目标co_routine的上下文.

我们先简要看一下协程上下文的定义

//coctx.h
struct coctx_t
{
#if defined(__i386__) //忽略
    void *regs[ 8 ];
#else
    void *regs[ 14 ];//用于保存或者设定特定寄存器值
#endif
    size_t ss_size;//栈帧区域的size
    char *ss_sp; //协程栈帧内存区域,这个区域一般在堆上分配
};

14个寄存器,每个寄存器是64位(8字节),所以寄存器组最后一个位置偏移112=13*8字节(下面要用到)

先简单解释一下头部的代码:

.globl coctx_swap   //.global 声明coctx_swap是全局可见的
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type  coctx_swap, @function    //gnu汇编器定义函数时的规则
#endif
coctx_swap:                     //coctx_swap函数内容开始
    leaq 8(%rsp),%rax
    leaq 112(%rdi),%rsp
    ...

该函数实际被调用时,传入了两个参数,均为coctx_t类型指针。接下来我们看该函数的上半段

lea是取址指令,b,w,l,q是操作属性限定符,分别表示1字节,2字节,4字节,8字节。

#elif defined(__x86_64__)
	//进入函数之前的栈的情况
	//(old) rip 
	//rbp = rsp 
	//刚开始进入,rbp = rsp 
	leaq 8(%rsp),%rax // rax=rsp+8,rax 保存了原来的 rip 
	leaq 112(%rdi),%rsp //rsp 指向原先的 rbp
	pushq %rax //保存各个寄存器 
	pushq %rbx
	pushq %rcx
	pushq %rdx

	pushq -8(%rax) //ret func addr返回原来的 rip 

	pushq %rsi
	pushq %rdi
	pushq %rbp
	pushq %r8
	pushq %r9
	pushq %r12
	pushq %r13
	pushq %r14
	pushq %r15
	.....

上半段相当于是把当前的各寄存器值存入了第一个参数传入的协程上下文的regs数组中最后的返回地址放在了regs[9]中,rsi会保存着第二个参数 coctx_t

ok,来看下半部分 (恢复下一个工作协程的上下文):

	movq %rsi, %rsp//rsp(存储栈顶的地址,改变它的地址,就相当于改变了栈空间)替换为rsi寄存器中的值
	popq %r15//regs数组中的各值恢复到各寄存器中。将返回地址压入栈中
	popq %r14
	popq %r13
	popq %r12
	popq %r9
	popq %r8
	popq %rbp
	popq %rdi
	popq %rsi
	popq %rax //ret func addr regs[9] =  rax 
	popq %rdx
	popq %rcx
	popq %rbx
	popq %rsp
	pushq %rax //入栈 rax 
	
	xorl %eax, %eax
	ret //ret指令约定从当前栈顶弹出返回地址放入IP寄存器,让出控制流。

OK,至此,切换就完成了!!!

windows 8086 汇编实现切换

这是一位大佬写的,我也看不太懂,正在学习 ing …

yield proc
		pushf	;先保存一下flags
		cmp ax,0 ;判断是否是esc
		jne yEls1
			mov AH, 4CH	;esc exit
      			int 21h	
	yEls1:
		;是tab,先在它自己的栈里保存寄存器,
		push ax
		push bx
		push cx
		push dx
		push di
		push si
		push bp
		
		;再切换栈,恢复另一组寄存器
		cmp yFunc,0	;当前是tri?
		je yTri

			mov stack_snake_sp,sp
			mov sp,stack_tri_sp		;当前是1:snake,要切换成tri
			mov ax,STACK_RTI
			mov ss,ax		
			mov yFunc,0

			jmp yTriEd
	yTri:
			mov stack_tri_sp,sp
			mov sp,stack_snake_sp		;当前是0:tri,要切换成snake
			mov ax,STACK_SNAKE
			mov ss,ax
			mov yFunc,1
		;恢复寄存器
	yTriEd:

		pop bp
		pop si
		pop di
		pop dx
		pop cx
		pop bx
		pop ax
		popf
		ret	;切换
yield endp
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值