在分析了各大開源協(xié)程庫實現(xiàn)后翰意,最終選擇參考boost.context的匯編實現(xiàn)约郁,來寫tbox的切換內(nèi)核。
在這過程中流昏,我對boost各個架構(gòu)平臺下的context切換扎即,都進(jìn)行了分析和測試吞获。
在macosx i386和mips平臺上實現(xiàn)協(xié)程切換時,發(fā)現(xiàn)boost那套匯編實現(xiàn)是有問題的铺遂,如果放到tbox切換demo上運行衫哥,會直接掛掉茎刚。
在分析這兩個架構(gòu)上襟锐,boost.context切換實現(xiàn)問題,這邊先貼下tbox上的context切換demo膛锭,方便之后的講解:
static tb_void_t func1(tb_context_from_t from)
{
// check
tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv;
tb_assert_and_check_return(contexts);
// 先保存下主函數(shù)入口context粮坞,方便之后切換回去
contexts[0] = from.context;
// 初始化切換到func2
from.context = contexts[2];
// loop
tb_size_t count = 10;
while (count--)
{
// trace
tb_trace_i("func1: %lu", count);
// 切換到func2,返回后更新from中的context地址
from = tb_context_jump(from.context, contexts);
}
// 切換回主入口函數(shù)
tb_context_jump(contexts[0], tb_null);
}
static tb_void_t func2(tb_context_from_t from)
{
// check
tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv;
tb_assert_and_check_return(contexts);
// loop
tb_size_t count = 10;
while (count--)
{
// trace
tb_trace_i("func2: %lu", count);
// 切換到func1初狰,返回后更新from中的context地址
from = tb_context_jump(from.context, contexts);
}
// 切換回主入口函數(shù)
tb_context_jump(contexts[0], tb_null);
}
static tb_void_t test()
{
// 定義全局堆棧
static tb_context_ref_t contexts[3];
static tb_byte_t stacks1[8192];
static tb_byte_t stacks2[8192];
// 生成兩個context上下文莫杈,綁定對應(yīng)函數(shù)和堆棧
contexts[1] = tb_context_make(stacks1, sizeof(stacks1), func1);
contexts[2] = tb_context_make(stacks2, sizeof(stacks2), func2);
// 切換到func1并傳遞私有參數(shù):context數(shù)組
tb_context_jump(contexts[1], contexts);
}
這里為了測試context切換,直接使用的底層切換接口tb_context_make
和tb_context_jump
奢入,所以代碼使用上筝闹,比較原始。
這兩個接口相當(dāng)于boost的make_fcontext
和jump_fcontext
腥光,當(dāng)然實際應(yīng)用中关顷,tbox的協(xié)程庫提供了更上層的封裝,并不會直接使用這兩個接口武福。
這個demo很簡單议双,就是創(chuàng)建兩個context,來回切換捉片,最后結(jié)束返回到主函數(shù)平痰。
然后再直接嘗試使用boost的實現(xiàn)時,出現(xiàn)了兩個不同現(xiàn)象的crash
- macosx i386下伍纫,從func2切換回到func1時發(fā)生了崩潰
- mips32下宗雇,在執(zhí)行完10次來回切換后,切回主函數(shù)是莹规,發(fā)生了崩潰
macosx i386下的問題分析
我們先來分析下macosx i386的這個問題赔蒲,由于之前tbox已經(jīng)參考了boost的linux i386下的實現(xiàn),完成了上下文切換访惜,是能正常運行的嘹履。
因此,可以在這兩個平臺下做下對比债热,結(jié)果發(fā)現(xiàn)砾嫉,boost幾乎是直接照搬了linux下那套實現(xiàn),那么問題來了窒篱,為甚了linux下ok焕刮,macosx上就有問題呢舶沿。
大體可以猜到,應(yīng)該是調(diào)用棧布局的不同導(dǎo)致的問題配并,因此我們看下macosx上的boost jump實現(xiàn):
.text
.globl _jump_fcontext
.align 2
_jump_fcontext:
pushl %ebp /* save EBP */
pushl %ebx /* save EBX */
pushl %esi /* save ESI */
pushl %edi /* save EDI */
/* store fcontext_t in ECX */
movl %esp, %ecx
/* first arg of jump_fcontext() == context jumping to */
movl 0x18(%esp), %eax
/* second arg of jump_fcontext() == data to be transferred */
movl 0x1c(%esp), %edx
/* restore ESP (pointing to context-data) from EAX */
movl %eax, %esp
/* address of returned transport_t */
movl 0x14(%esp), %eax
/* return parent fcontext_t */
movl %ecx, (%eax)
/* return data */
movl %edx, 0x4(%eax)
popl %edi /* restore EDI */
popl %esi /* restore ESI */
popl %ebx /* restore EBX */
popl %ebp /* restore EBP */
/* jump to context */
ret $4
jump_fcontext
的參數(shù)原型是:struct(context, data) = jump_fcontext(context, data)
括荡,跟tbox的tb_context_jump
差不多
都是傳入一個struct,相當(dāng)于傳入了兩個參數(shù)溉旋,一個context畸冲,一個data,返回結(jié)果也是一個類似struct
而從上面的代碼中可以看到观腊,從esp + 0x18處取了第一個參數(shù)context邑闲,esp + 0x1c取得是第二個參數(shù)data,換算到_jump_fcontext的入口處
可以確定出_jump_fcontext入口處大體的棧布局:
esp + 12: data參數(shù)
esp + 8: context參數(shù)
esp + 4: ??
esp : _jump_fcontext的返回地址
按照i386的調(diào)用棧布局梧油,函數(shù)入口處第一個參數(shù)苫耸,應(yīng)該是通過 esp + 4 訪問的,那為什么context參數(shù)卻是在esp + 8處呢儡陨,esp + 4指向的內(nèi)容又是什么褪子?
我們可以看下,_jump_fcontext調(diào)用處的匯編偽代碼:
pushl data
pushl context
pushl hidden
call _jump_fcontext
addl $12, %esp
其實編譯器在調(diào)用_jump_fcontext處骗村,實際壓入了三個參數(shù)嫌褪,這個esp + 4指向的hidden數(shù)據(jù),這個是_jump_fcontext返回的struct數(shù)據(jù)的椥鹕恚空間地址
用于在_jump_fcontext內(nèi)部渔扎,設(shè)置返回struct(context, data)的數(shù)據(jù),也就是:
/* address of returned transport_t */
movl 0x14(%esp), %eax
/* return parent fcontext_t */
movl %ecx, (%eax)
/* return data */
movl %edx, 0x4(%eax)
說白了信轿,linux i386上返回struct數(shù)據(jù)晃痴,是通過傳入一個指向棧空間的變量指針财忽,作為隱藏的第一個參數(shù)倘核,用于設(shè)置struct數(shù)據(jù)返回。
而boost在macosx i386上即彪,也直接照搬了這種布局來實現(xiàn)紧唱,那macosx上是否真的也是這么做的呢?
我們來寫個測試程序驗證下:
static tb_context_from_t test()
{
tb_context_from_t from = {0};
return from;
}
反匯編后的結(jié)果如下:
__text:00051BD0 _test proc near
__text:00051BD0
__text:00051BD0 var_10 = dword ptr -10h
__text:00051BD0 var_C = dword ptr -0Ch
__text:00051BD0 var_8 = dword ptr -8
__text:00051BD0 var_4 = dword ptr -4
__text:00051BD0
__text:00051BD0 push ebp
__text:00051BD1 mov ebp, esp
__text:00051BD3 sub esp, 10h
__text:00051BD6 mov [ebp+var_C], 0
__text:00051BDD mov [ebp+var_10], 0
__text:00051BE4 mov [ebp+var_4], 0
__text:00051BEB mov [ebp+var_8], 0
__text:00051BF2 mov eax, [ebp+var_8]
__text:00051BF5 mov edx, [ebp+var_4]
__text:00051BF8 add esp, 10h
__text:00051BFB pop ebp
__text:00051BFC retn
__text:00051BFC _test endp
可以看到隶校,實際上并沒有像linux上那樣通過一個struct指針來返回漏益,而是直接將struct(context, data),通過 eax, edx 進(jìn)行返回深胳。
到這里绰疤,我們大概可以猜到,macosx上舞终,對這種小的struct結(jié)構(gòu)體返回做了優(yōu)化轻庆,直接放置在了eax癣猾,edx中,而我們的from結(jié)構(gòu)體只有兩個pointer余爆,正好滿足這種方式纷宇。
因此,為了修復(fù)macosx上的問題蛾方,tbox在實現(xiàn)上像捶,對棧布局做了調(diào)整,并且做了些額外的優(yōu)化:
1. 調(diào)整jump實現(xiàn)转捕,改用eax作岖,edx直接返回from結(jié)構(gòu)體
2. 由于不再像linux那樣通過保留一個額外的椝衾空間返回struct五芝,可以把linux那種跳板實現(xiàn)去掉,改為直接jump到實際位置(提升切換效率)
mips32下的問題分析
mips下這個問題辕万,我之前也是調(diào)試了很久枢步,在每次切換完成后,打算切換回主函數(shù)時渐尿,就會發(fā)生crash醉途,也就是下面這個位置:
static tb_void_t func1(tb_context_from_t from)
{
// check
tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv;
tb_assert_and_check_return(contexts);
// 先保存下主函數(shù)入口context,方便之后切換回去
contexts[0] = from.context;
// 初始化切換到func2
from.context = contexts[2];
// loop
tb_size_t count = 10;
while (count--)
{
// trace
tb_trace_i("func1: %lu", count);
// 切換到func2砖茸,返回后更新from中的context地址
from = tb_context_jump(from.context, contexts);
}
// 切換回主入口函數(shù)
tb_context_jump(contexts[0], tb_null); <----- 此處發(fā)生崩潰
}
我們先來初步分析下隘擎,既然之前的來回切換都是ok的,只有在最后這個切換發(fā)生問題凉夯,那么可以確定jump的大體實現(xiàn)應(yīng)該還是ok的
可能是傳入jump的參數(shù)不對導(dǎo)致的問題货葬,最有可能的是 contexts[0] 指向的主函數(shù)上下文地址已經(jīng)不對了。
通過printf確認(rèn)劲够,確實值不對了震桶,那么在func1入口處這個contexts[0],是否正確呢征绎,我又繼續(xù)printf了下蹲姐,居然還是不對。 = =
然后人柿,我又繼續(xù)打印contexts[0], contexts[1], contexts[2]
這三個在func1入口處的值柴墩,發(fā)現(xiàn)只有contexts[2]是對的
前兩處都不對了,而且值得注意的是凫岖,這兩個的值江咳,正好是from.context和from.data的值。
由此隘截,可以得出一個初步結(jié)論:
1. contexts這塊buffer的前兩處數(shù)據(jù)扎阶,在jump切換到func1的時候被自動改寫了
2. 而且改寫后的數(shù)據(jù)值汹胃,正好是from里面的context和data
說白了,也就是發(fā)生越界了东臀。着饥。
那什么情況下, contexts指向的數(shù)據(jù)會發(fā)生越界呢,可以先看下contexts的定義:
static tb_void_t test()
{
// 定義全局堆棧
static tb_context_ref_t contexts[3];
static tb_byte_t stacks1[8192];
static tb_byte_t stacks2[8192];
// 生成兩個context上下文惰赋,綁定對應(yīng)函數(shù)和堆棧
contexts[1] = tb_context_make(stacks1, sizeof(stacks1), func1);
contexts[2] = tb_context_make(stacks2, sizeof(stacks2), func2);
// 切換到func1并傳遞私有參數(shù):context數(shù)組
tb_context_jump(contexts[1], contexts);
}
contexts[3]的數(shù)據(jù)定義宰掉,正好在stacks1的上面,而stacks1是作為func1的堆棧傳入的赁濒,也就是說轨奄,如果func1的堆棧發(fā)生上溢,就會擦掉contexts里面的數(shù)據(jù)拒炎。
我們接著來看下挪拟,boost的實現(xiàn),看看是否有地方會發(fā)生這種情況:
.text
.globl make_fcontext
.align 2
.type make_fcontext,@function
.ent make_fcontext
make_fcontext:
#ifdef __PIC__
.set noreorder
.cpload $t9
.set reorder
#endif
# first arg of make_fcontext() == top address of context-stack
move $v0, $a0
# shift address in A0 to lower 16 byte boundary
move $v1, $v0
li $v0, -16 # 0xfffffffffffffff0
and $v0, $v1, $v0
# reserve space for context-data on context-stack
# including 48 byte of shadow space (sp % 16 == 0)
addiu $v0, $v0, -112
# third arg of make_fcontext() == address of context-function
sw $a2, 44($v0)
# save global pointer in context-data
sw $gp, 48($v0)
# compute address of returned transfer_t
addiu $t0, $v0, 52
sw $t0, 36($v0)
# compute abs address of label finish
la $t9, finish
# save address of finish as return-address for context-function
# will be entered after context-function returns
sw $t9, 40($v0)
jr $ra # return pointer to context-data
finish:
lw $gp, 0($sp)
# allocate stack space (contains shadow space for subroutines)
addiu $sp, $sp, -32
# save return address
sw $ra, 28($sp)
# restore GP (global pointer)
# move $gp, $s1
# exit code is zero
move $a0, $zero
# address of exit
lw $t9, %call16(_exit)($gp)
# exit application
jalr $t9
.end make_fcontext
.size make_fcontext, .-make_fcontext
.text
.globl jump_fcontext
.align 2
.type jump_fcontext,@function
.ent jump_fcontext
jump_fcontext:
# reserve space on stack
addiu $sp, $sp, -112
sw $s0, ($sp) # save S0
sw $s1, 4($sp) # save S1
sw $s2, 8($sp) # save S2
sw $s3, 12($sp) # save S3
sw $s4, 16($sp) # save S4
sw $s5, 20($sp) # save S5
sw $s6, 24($sp) # save S6
sw $s7, 28($sp) # save S7
sw $fp, 32($sp) # save FP
sw $a0, 36($sp) # save hidden, address of returned transfer_t
sw $ra, 40($sp) # save RA
sw $ra, 44($sp) # save RA as PC
# store SP (pointing to context-data) in A0
move $a0, $sp
# restore SP (pointing to context-data) from A1
move $sp, $a1
lw $s0, ($sp) # restore S0
lw $s1, 4($sp) # restore S1
lw $s2, 8($sp) # restore S2
lw $s3, 12($sp) # restore S3
lw $s4, 16($sp) # restore S4
lw $s5, 20($sp) # restore S5
lw $s6, 24($sp) # restore S6
lw $s7, 28($sp) # restore S7
lw $fp, 32($sp) # restore FP
lw $t0, 36($sp) # restore hidden, address of returned transfer_t
lw $ra, 40($sp) # restore RA
# load PC
lw $t9, 44($sp)
# adjust stack
addiu $sp, $sp, 112
# return transfer_t from jump
sw $a0, ($t0) # fctx of transfer_t
sw $a1, 4($t0) # data of transfer_t
# pass transfer_t as first arg in context function
# A0 == fctx, A1 == data
move $a1, $a2
# jump to context
jr $t9
.end jump_fcontext
.size jump_fcontext, .-jump_fcontext
可以看到击你,boost在make_fcontext的時候玉组,先對傳入的棧頂做了16字節(jié)的對齊,然后保留了112字節(jié)的空間丁侄,用于保存寄存器數(shù)據(jù)惯雳。
然后再jump切換到新context的時候,恢復(fù)了新context所需的寄存器鸿摇,并把新的sp指針+112石景,把保留的棧空間給pop了拙吉。
也就是說潮孽,在第一次切換到實際func1函數(shù)入口時,這個時候的棧指針指向棧頂?shù)穆洌偻隙魃蹋呀?jīng)沒有多少空間了(也就只有為了16字節(jié)對齊,有可能保留的少部分空間)必逆。
換一句話說怠堪,如果傳入的stack1的棧頂本身就是16字節(jié)對齊的,那么func1的入口處sp指向的就是stack1的棧頂
如果在func1的入口處名眉,有超過stack1棧頂范圍的寫操作粟矿,就有可能會擦掉contexts的數(shù)據(jù),因為contexts緊靠著stack1的棧頂位置损拢。
那是否會出現(xiàn)這種情況陌粹,我們通過反匯編func1的入口處代碼,實際看下:
.text:00453F04 func1:
.text:00453F04
.text:00453F04 var_30 = -0x30
.text:00453F04 var_2C = -0x2C
.text:00453F04 var_28 = -0x28
.text:00453F04 var_20 = -0x20
.text:00453F04 var_18 = -0x18
.text:00453F04 var_14 = -0x14
.text:00453F04 var_10 = -0x10
.text:00453F04 var_8 = -8
.text:00453F04 var_4 = -4
.text:00453F04 arg_0 = 0
.text:00453F04 arg_4 = 4
.text:00453F04
.text:00453F04 addiu $sp, -0x40
.text:00453F08 sw $ra, 0x40+var_4($sp)
.text:00453F0C sw $fp, 0x40+var_8($sp)
.text:00453F10 move $fp, $sp
.text:00453F14 la $gp, unk_5706A0
.text:00453F1C sw $gp, 0x40+var_20($sp)
.text:00453F20 sw $a0, 0x40+arg_0($fp) <------------ 此處發(fā)生越界福压,改寫了contexts[0] = from.context
.text:00453F24 sw $a1, 0x40+arg_4($fp) <------------ 此處發(fā)生越界掏秩,改寫了contexts[1] = from.data
.text:00453F28 lw $v0, 0x40+arg_4($fp)
.text:00453F2C sw $v0, 0x40+var_14($fp)
.text:00453F30 lw $v0, 0x40+var_14($fp)
.text:00453F34 sltu $v0, $zero, $v0
.text:00453F38 andi $v0, 0xFF
.text:00453F3C move $v1, $v0
可以看到或舞,確實發(fā)生了越界行為,那為什么在函數(shù)內(nèi)部蒙幻,還會去寫當(dāng)前棧幀外的數(shù)據(jù)呢映凳,這個要從mips的調(diào)用棧布局上說起了。
簡單來說邮破,mips在調(diào)用某個函數(shù)時诈豌,會把a(bǔ)0-a3作為參數(shù)寄存器,其他參數(shù)放置在堆棧中抒和,但是與其他架構(gòu)有點不同的是:
mips還會去為a0-a3這前四個參數(shù)矫渔,保留棧空間
調(diào)用棧如下:
------------
| other args |
|------------|
| a0-a3 | <- 參數(shù)傳遞使用a0-a3摧莽,但是還是會為這四個參數(shù)保留椕硗荩空間出來
|------------|
| ra | <- 返回地址
|------------|
| fp gp s0-7 | <- 保存的一些其他寄存器
|------------|
| locals |
------------
而剛剛在func1內(nèi),就是回寫了a0-a3
處保留的椃吨觯空間送膳,導(dǎo)致了越界,因為boost的實現(xiàn)在jump后丑蛤,棧空間已經(jīng)到棧頂了撕阎,空間不夠了受裹。脯厨。
因此智哀,為了修復(fù)這個問題镶摘,只需要在make_fcontext
里面遇西,多保留a0-a3
這32字節(jié)的空間就行了洛退,也就是:
.globl make_fcontext
# reserve space for context-data on context-stack
# including 48 byte of shadow space (sp % 16 == 0)
# addiu $v0, $v0, -112
addiu $v0, $v0, -146
而在tbox內(nèi)干奢,除了對此處的額外的椊染椋空間保留彼水,來修復(fù)此問題汗侵,還對棧數(shù)據(jù)進(jìn)行了更加合理的分配利用幸缕,不再需要保留146這么多字節(jié)數(shù)
只需要保留96字節(jié),就夠用了晰韵,節(jié)省了50個字節(jié)发乔,如果同時存在1024個協(xié)程的話,相當(dāng)于節(jié)省了50K的內(nèi)存數(shù)據(jù)雪猪。
并且boost的jump實現(xiàn)上栏尚,還有其他兩處問題,tbox里面一并修復(fù)了:
jump_fcontext:
# reserve space on stack
addiu $sp, $sp, -112
sw $s0, ($sp) # save S0
sw $s1, 4($sp) # save S1
sw $s2, 8($sp) # save S2
sw $s3, 12($sp) # save S3
sw $s4, 16($sp) # save S4
sw $s5, 20($sp) # save S5
sw $s6, 24($sp) # save S6
sw $s7, 28($sp) # save S7
sw $fp, 32($sp) # save FP
sw $a0, 36($sp) # save hidden, address of returned transfer_t
sw $ra, 40($sp) # save RA
sw $ra, 44($sp) # save RA as PC
<-------------------- 此處boost雖然為gp保留了48($sp)空間只恨,但是確沒去保存gp寄存器
# store SP (pointing to context-data) in A0
move $a0, $sp
# restore SP (pointing to context-data) from A1
move $sp, $a1
lw $s0, ($sp) # restore S0
lw $s1, 4($sp) # restore S1
lw $s2, 8($sp) # restore S2
lw $s3, 12($sp) # restore S3
lw $s4, 16($sp) # restore S4
lw $s5, 20($sp) # restore S5
lw $s6, 24($sp) # restore S6
lw $s7, 28($sp) # restore S7
lw $fp, 32($sp) # restore FP
lw $t0, 36($sp) # restore hidden, address of returned transfer_t
lw $ra, 40($sp) # restore RA
<-------------------- 此處boost也沒去恢復(fù)gp寄存器
# load PC
lw $t9, 44($sp)
# adjust stack
addiu $sp, $sp, 112
# return transfer_t from jump
sw $a0, ($t0) # fctx of transfer_t
sw $a1, 4($t0) # data of transfer_t <------------- 此處應(yīng)該使用 a2 而不是 a1
# pass transfer_t as first arg in context function
# A0 == fctx, A1 == data
move $a1, $a2
# jump to context
jr $t9
.end jump_fcontext
最后說一下译仗,本文是針對boost 1.62.0 版本做的分析抬虽,如有不對之處,歡迎指正哈纵菌。斥赋。
個人主頁:TBOOX開源工程
原文出處:http://tboox.org/cn/2016/11/13/boost-context-bug/