SRS是單進(jìn)程、單線程、多協(xié)程結(jié)構(gòu),協(xié)程(coroutine)背景以后再介紹,這篇文章介紹協(xié)程的重要基礎(chǔ),理解了這個(gè)基礎(chǔ),后續(xù)就容易看懂協(xié)程,也能更好的使用協(xié)程。
SRS的線程模型,未來(lái)會(huì)改進(jìn)成
單進(jìn)程、多線程、多協(xié)程架構(gòu),相關(guān)背景和原因請(qǐng)看#2188。
協(xié)程就是用戶空間的輕量線程,或者說(shuō)是用戶空間創(chuàng)建的偽線程,既然是創(chuàng)建了線程,就需要實(shí)現(xiàn)函數(shù)調(diào)用。簡(jiǎn)單來(lái)說(shuō),協(xié)程和線程切換的過(guò)程是類似的,只不過(guò)是用戶空間實(shí)現(xiàn)的切換:
-
_st_md_cxt_save:保存當(dāng)前函數(shù)信息信息到內(nèi)存,后續(xù)可以跳轉(zhuǎn)到這個(gè)函數(shù)。 -
_st_md_cxt_restore:從內(nèi)存恢復(fù)函數(shù)的信息,跳轉(zhuǎn)到這個(gè)協(xié)程。
那么到底需要保存什么信息,又需要恢復(fù)哪些信息?這就涉及到了函數(shù)是如何調(diào)用的,寄存器都用來(lái)保存什么信息。
Code
我們寫個(gè)簡(jiǎn)單的函數(shù):
// g++ frame0.cpp -g -O0 -o frame && gdb frame
#include <stdio.h>
#include <stdlib.h>
int callee(int a, long b) {
int c = a;
c += (int)b;
return c;
}
void caller() {
int v = callee(10, 20);
printf("v=%d\n", v);
}
int main(int argc, char** argv) {
caller();
return 0;
}
代碼可以直接從Docker中獲取、編譯和調(diào)試:
docker run --rm --privileged -it -w /srs/trunk/research/frame \
ossrs/srs:study cat frame0.cpp
# 國(guó)內(nèi)建議用阿里云的鏡像
docker run --rm --privileged -it -w /srs/trunk/research/frame \
registry.cn-hangzhou.aliyuncs.com/ossrs/srs:study \
cat frame0.cpp
Docker的使用詳細(xì)請(qǐng)參考srs-docker: study。
GDB
編譯代碼,用GDB啟動(dòng)調(diào)試:
docker run --rm --privileged -it -w /srs/trunk/research/frame \
registry.cn-hangzhou.aliyuncs.com/ossrs/srs:study \
bash -c 'g++ frame0.cpp -g -O0 -o frame && gdb frame'
設(shè)置斷點(diǎn)在main函數(shù):
(gdb) b main
(gdb) run
關(guān)于常用匯編的GDB指令:
-
layout pre:切換到TUI(文本圖形模式),可以多次切換選擇不同的layout,可以看到匯編和寄存器。 -
CTRL + x:快捷鍵,在TUI和非TUI模式下切換;可以配合layout pre使用。 -
si:匯編指令單步執(zhí)行,每次只執(zhí)行一行匯編。由于一行C代碼可能對(duì)應(yīng)多行匯編,所以函數(shù)調(diào)用時(shí)需要看每行匯編的執(zhí)行。 -
p $rax或p /x $rax:查看寄存器rax的內(nèi)容。 -
x /2xa 0x7ffe490993d8:查看內(nèi)存塊中的指針,以8字節(jié)為單元查看。
如下圖所示,切換到寄存器模式:
搭建好環(huán)境,我們就可以分析執(zhí)行函數(shù)都調(diào)用了哪些匯編,寄存器又有和變化。
函數(shù)調(diào)用過(guò)程
分析caller()調(diào)用callee()函數(shù)的匯編代碼:
0x40058c <caller()+18> callq 0x40055d <callee(int, long)>
callq這個(gè)指令,自動(dòng)保存了caller的rip到棧:
# 執(zhí)行callq之前,棧rsp是
$rsp = (void *) 0x7ffe490993e0
# 執(zhí)行callq這條匯編之后,棧向下移動(dòng)了8字節(jié):
$rsp = (void *) 0x7ffe490993d8
# 可以看到,是將rip值保存到了棧,也就是caller的入口地址:
(gdb) x/1xa 0x7ffe490993d8
0x7ffe490993d8: 0x400591 <caller()+23>
進(jìn)入callee函數(shù)時(shí),有兩條匯編做了初始化:
# 將rbp,這時(shí)候還是caller的rbp放到堆棧
0x40055d <callee(int, long)> push %rbp
# 將rsp也就是callee函數(shù)當(dāng)前的棧,放入rbp
0x40055e <callee(int, long)+1> mov %rsp,%rbp
# 執(zhí)行后,棧繼續(xù)向下移動(dòng)8字節(jié)(push指令),并設(shè)置了rbp
$rsp = (void *) 0x7ffe490993d0
$rbp = (void *) 0x7ffe490993d0
此時(shí),棧中就保存了兩個(gè)重要的信息,就是caller的rip和rbp:
(gdb) x/2xa 0x7ffe490993d0
0x7ffe490993d0: 0x7ffe490993f0 0x400591 <caller()+23>
總結(jié)如下圖所示:
callee的
rbp前兩個(gè)指針,16字節(jié),就是caller的rip和fp/rbp。
為何要保存這個(gè)信息呢?這兩個(gè)信息實(shí)際上就是函數(shù)的入口和棧地址,也可以在函數(shù)中獲取調(diào)用堆棧。比如,我們進(jìn)入callee后,根據(jù)這兩個(gè)信息,可以知道整個(gè)調(diào)用鏈:
# 在callee中,查看callee的`rbp`指向的棧的兩個(gè)指針
# rip 0x400591,就是caller的入口
# rbp 0x7ffe490993f0,就是caller的rbp
(gdb) x/2xa $rbp
0x7ffe490993d0: 0x7ffe490993f0 0x400591 <caller()+23>
# 由于知道了caller的rbp,可以繼續(xù)查看上一層的調(diào)用信息:
# rip 0x4005be,就是main函數(shù)
# rbp 0x7ffe49099410,就是main的rbp了
(gdb) x/2xa 0x7ffe490993f0
0x7ffe490993f0: 0x7ffe49099410 0x4005be <main(int, char**)+20>
# 還可以繼續(xù)查看,最終入口是glibc的這個(gè)函數(shù):
(gdb) x/2xa 0x7ffe49099410
0x7ffe49099410: 0x0 0x7f1608e8f555 <__libc_start_main+245>
為了方便,還有個(gè)
fp寄存器,一般就等于rbp,但是并非所有都是這么實(shí)現(xiàn)。
我們?cè)趃db中,一般通過(guò)bt查看調(diào)用堆棧,顯示的地址就是rip:
(gdb) bt
#0 0x000000000040056b in callee (a=10, b=20) at frame0.cpp:9
#1 0x0000000000400591 in caller () at frame0.cpp:15
#2 0x00000000004005be in main (argc=1, argv=0x7ffec0b642c8) at frame0.cpp:20
(gdb) p $rip
$45 = (void (*)(void)) 0x40056b <callee(int, long)+14>
(gdb) x/2xa $rbp
0x7ffec0b641a0: 0x7ffec0b641c0 0x400591 <caller()+23>
關(guān)于參數(shù)rdi/rsi/rdx/rcx/r8/r9和返回值rax,我們以另外一個(gè)例子說(shuō)明。
長(zhǎng)參數(shù)函數(shù)調(diào)用
下面是一個(gè)有很多參數(shù)的程序的例子:
docker run --rm --privileged -it -w /srs/trunk/research/frame \
registry.cn-hangzhou.aliyuncs.com/ossrs/srs:study \
cat cat frame1.cpp
編譯代碼,用GDB啟動(dòng)調(diào)試:
docker run --rm --privileged -it -w /srs/trunk/research/frame \
registry.cn-hangzhou.aliyuncs.com/ossrs/srs:study \
bash -c 'g++ frame1.cpp -g -O0 -o frame && gdb frame'
調(diào)試后,總結(jié)如下圖所示:
- 返回值是
rax。 - 第一個(gè)參數(shù)
rdi,第二個(gè)rsi,第三個(gè)是rdx,第四個(gè)是rcx,第五個(gè)是r8,第六個(gè)是r9,再往后就在rsp堆棧往上存儲(chǔ)。