摘要:所以子程序調用是通過棧實現的,子程序調用總是一個入口,一次返回,調用順序是明確的。棧保存的數據,指令通過寄存器控制。所以需要多個棧來維護。由于棧是從高到底,此處的意思表示預留字節的棧空間。
我們先弄清如何進行協程的切換,程序可以在某個地方掛起,跳轉到另外的流程中執行,并且可以重新在掛起處繼續運行。那如何實現呢?
我們先來看一個例子,有下面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字節的??臻g。 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 的補碼表示為0xfffffffff0.
/* 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字節??臻g,將第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 字節。
make_fcontext 創建協程的棧。jump_fcontext實現跳轉。
網校內部培訓視頻by李樂 https://biglive.xueersi.com/L...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31450.html
閱讀 1118·2021-11-25 09:43
閱讀 1640·2021-09-13 10:25
閱讀 2592·2021-09-09 11:38
閱讀 3400·2021-09-07 10:14
閱讀 1714·2019-08-30 15:52
閱讀 641·2019-08-30 15:44
閱讀 3572·2019-08-29 13:23
閱讀 1974·2019-08-26 13:33