協(xié)程原理:函數(shù)調(diào)用過(guò)程、參數(shù)和寄存器

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 $raxp /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)保存了callerrip到棧:

# 執(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的riprbp

(gdb) x/2xa 0x7ffe490993d0
0x7ffe490993d0: 0x7ffe490993f0  0x400591 <caller()+23>

總結(jié)如下圖所示:

callee的rbp前兩個(gè)指針,16字節(jié),就是caller的ripfp/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é)如下圖所示:

  1. 返回值是rax。
  2. 第一個(gè)參數(shù)rdi,第二個(gè)rsi,第三個(gè)是rdx,第四個(gè)是rcx,第五個(gè)是r8,第六個(gè)是r9,再往后就在rsp堆棧往上存儲(chǔ)。
還有 1% 的精彩內(nèi)容
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
支付 ¥1.00 繼續(xù)閱讀

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容