乐趣区

makefcountextjumpfcontext

我们先弄清如何进行协程的切换,程序可以在某个地方挂起,跳转到另外的流程中执行,并且可以重新在挂起处继续运行。那如何实现呢?

我们先来看一个例子,有下面 2 个函数,如果在一个单线程中让输出结果依次是 funcA1 funcB1 funcA2 funcB2 … , 你会怎么做呢?

void funcA(){
    int i = 0;
    while(true){
        //to do something
        
        printf("funcA%d",i);
        i++;
    }
}

void funcB(){
    int i = 0;
    while(true){
        //to do something
        
        printf("funcB%d",i);
        i++;
    }
}

如果从 c 代码的角度来看,如果单线程运行到 func1 的 while 循环中,如何能调用到 func2 的 while 循环呢?必须使用跳转。

首先想到是 goto。goto 是可以实现跳转,但是 goto 不能实现函数间的跳转。无法满足这个要求。即使可以实现函数间跳转,难道就可行吗?

那这里不得不说下 C 函数调用过程

具体相见这篇文章 https://blog.csdn.net/jelly_9/article/details/53239718
子程序或者称为函数,在所有语言中都是层级调用,比如 A 调用 B,B 在执行过程中又调用了 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕。所以子程序调用是通过栈实现的,子程序调用总是一个入口,一次返回,调用顺序是明确的。

程序运行有 2 个部分,指令,数据。栈保存的数据,指令通过寄存器(rip)控制。

 2 个函数内部的跳转必须保证栈是正确,所以跳转之前需要保存好当前的栈信息,然后跳转。另外我们可以得到另一个信息,在一个栈上实现多个流程直接的跳转是不能实现的。所以需要多个栈来维护。

那我们来看 jump_fcontext 是怎么实现跳转

 c 语言函数声明
int jump_fcontext(fcontext_t *ofc, fcontext_t nfc, void* vp, bool preserve_fpu);


汇编代码如下
.text
.globl jump_fcontext
.type jump_fcontext,@function
.align 16
jump_fcontext:
    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

    /* test for flag preserve_fpu */
    cmp  $0, %rcx
    je  1f

    /* save MMX control- and status-word */
    stmxcsr  (%rsp)
    /* save x87 control-word */
    fnstcw   0x4(%rsp)

1:
    /* store RSP (pointing to context-data) in RDI */
    movq  %rsp, (%rdi)

    /* restore RSP (pointing to context-data) from RSI */
    movq  %rsi, %rsp

    /* test for flag preserve_fpu */
    cmp  $0, %rcx
    je  2f

    /* restore MMX control- and status-word */
    ldmxcsr  (%rsp)
    /* restore x87 control-word */
    fldcw  0x4(%rsp)

2:
    /* 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

/* Mark that we don't need executable stack.  */
.section .note.GNU-stack,"",%progbits

寄存器的用途可以先了解下 https://www.jianshu.com/p/571…

1、保存寄存器

pushq  %rbp  /* save RBP */
pushq  %rbx  /* save RBX */
pushq  %r15  /* save R15 */
pushq  %r14  /* save R14 */
pushq  %r13  /* save R13 */
pushq  %r12  /* save R12 */“被调函数有义务保证 rbp rbx r12~r15 这几个寄存器的值在进出函数前后一致”rbx 是基址寄存器 作用存放存储区的起始地址 被调用者保存
rbp (base pointer)基址指针寄存器,用于提供堆栈内某个单元的偏移地址,与 rss 段寄存器联用,可以访问堆栈中的任一个存储单元,被调用者保存

2、预留 fpu 8 个字节空间

/* prepare stack for FPU */
leaq  -0x8(%rsp), %rsp
表示 %rsp 中的内容减 8。由于栈是从高到底,此处的意思表示预留 8 字节的栈空间。FPU:(Float Point Unit,浮点运算单元)

3、判断是否保存 fpu

cmp  $0, %rcx
je  1f

rcx 是第四个参数,判断是否等于 0。如果为 0,跳转到 1 标示的位置。也就是 preserve_fpu。当 preserve_fpu = true 的时候,需要执行 2 个指令是将浮点型运算的 2 个 32 位寄存器数据保存到第 2 步中预留的 8 字节空间。/* save MMX control- and status-word */
stmxcsr  (%rsp)
/* save x87 control-word */
fnstcw   0x4(%rsp)

4、修改 rsp 此时已经改变到其他栈
将 rsp 保存到第一参数(第一个参数保存在 rdi)指向的内存。fcontext_t *ofc 第一参数 ofc 指向的内存中保存是 rsp 的指针。第二条指令,实现了将第二个参数复制到 rsp.

1:
/* store RSP (pointing to context-data) in RDI */
movq  %rsp, (%rdi)

/* restore RSP (pointing to context-data) from RSI */
movq  %rsi, %rsp

5、判断是否保存了 fpu,如果保存了就恢复保存在 nfx 栈上的 fpu 相关数据到响应的寄存器。

/* test for flag preserve_fpu */
cmp  $0, %rcx
je  2f

/* restore MMX control- and status-word */
ldmxcsr  (%rsp)
/* restore x87 control-word */
fldcw  0x4(%rsp)

6、将 rsp 存储的地址 +8(8 字节 fpu),按顺序将栈中数据恢复到寄存器中。

2:
/* 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 */

7、设置返回值,实现指令跳转。
接下来继续 pop 数据,那栈上存的是什么呢,在 c 函数调用文章中可以知道,call 的时候会保存 rip(指令寄存器)到栈。所以此时 POP 的数据是 rip 也就是下一条指令。这是下一条指令是 nfx 栈保存的,所以这是另一个协程的下一条指令。保存到 r8。最后跳转下一条指令就恢复到另一个协程运行 jmp *%r8。

movq %rdx, %rax 是将上一个协程 A jump_fcontext 第三个参数作为当前协程 B jump_fcontext 的返回值,可以实现 2 个协程直接的数据传递。

movq %rdx, %rdi 如果跳转过去的新的协程,将第三个参数作为协程 B 启动入口 void func(int param)的第一参数。

/* 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


了解了程序是如何跳转后,我门在看下如何创建一个协程栈呢。make_fcontext
c 语言函数声明 fcontext_t make_fcontext(void sp, size_t size, void (fn)(int));


.text
.globl make_fcontext
.type make_fcontext,@function
.align 16
make_fcontext:
    /* first arg of make_fcontext() == top of context-stack */
    movq  %rdi, %rax

    /* shift address in RAX to lower 16 byte boundary */
    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 */
    leaq  -0x48(%rax), %rax

    /* third arg of make_fcontext() == address of context-function */
    movq  %rdx, 0x38(%rax)

    /* save MMX control- and status-word */
    stmxcsr  (%rax)
    /* save x87 control-word */
    fnstcw   0x4(%rax)

    /* compute abs address of label finish */
    leaq  finish(%rip), %rcx
    /* save address of finish as return-address for context-function */
    /* will be entered after context-function returns */
    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. */
.section .note.GNU-stack,"",%progbits

1、第一个参数是程序申请的内存地址高位(栈是从高到低),将第一个参数放到 rax,将地址取 16 的整数倍。
andq $-16, %rax 表示低 4 位取 0。-16 的补码表示为 0xfffffffff00.

/* first arg of make_fcontext() == top of context-stack */
movq  %rdi, %rax

/* shift address in RAX to lower 16 byte boundary */
andq  $-16, %rax

2、预留 72 字节栈空间,将第 3 个参数(void (*fn)(int)函数指针)保存在当前偏移 0x38 位置(大小 8 字节)。

/* 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 */
leaq  -0x48(%rax), %rax

/* third arg of make_fcontext() == address of context-function */
movq  %rdx, 0x38(%rax)

3、保存 fpu 和 jump_fcontext 类似总大小 8 字节。

/* save MMX control- and status-word */
stmxcsr  (%rax)
/* save x87 control-word */
fnstcw   0x4(%rax)

4、计算 finish 的绝对地址,保存到栈的 0x40 位置。
leaq finish(%rip), %rcx 表示 finish 是相对位置 +rip 就是 finish 的函数的地址。

/* compute abs address of label finish */
leaq  finish(%rip), %rcx
/* save address of finish as return-address for context-function */
/* will be entered after context-function returns */
movq  %rcx, 0x40(%rax)

5、返回,rax 作为返回值,目前的指向可以当做新栈的栈顶,相当于 rsp

ret /* return pointer to context-data */

我们回头在看看为什么会预留 72 字节大小。首先知道 jump_fcontext 在新栈需要 pop 的大小为,fpu(8 字节)+ rbp rbx r12 ~ r15 (8*6 = 48 字节) = 56 字节。还会继续 POP rip 8 字节,所以可以看到第二步中 movq %rdx, 0x38(%rax),就是将 rip 保存到这个位置。
目前已经 64 字节了,栈还有存储什么呢,协程(fn 函数)运行完成后会退出调用 ret, 其实就是 POP 到 rip. 所以保存是 finish 函数指针 大小 8 字节。总共 72 字节。

退出移动版