天天看點

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

【1】協程簡介

IO 同步操作的邏輯代碼

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

IO 異步操作的邏輯代碼

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

IO 異步操作與 IO 同步操作對比

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

協程主要解決的問題,提供同時具有異步性能與同步代碼邏輯的解決方案;C/C++ 典型的協程庫 NtyCo,https://github.com/wangbojing/NtyCo;

【2】NtyCo 簡介

【2.1】NtyCo 相關 API 簡介

NtyCo 封裝了若幹接口,一類是協程本身的,一類是 posix 異步封裝協程 API;

協程相關 API 函數接口

協程建立
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg);

協程排程器運作
void nty_schedule_run(void);
           

POSIX 異步封裝 API 函數接口

建立 socke 執行個體
int nty_socket(int domain, int type, int protocol);

接收請求
int nty_accept(int fd, struct sockaddr *addr, socklen_t *len);

接收資料
ssize_t nty_recv(int fd, void *buf, size_t len, int flags);

發送資料
ssize_t nty_send(int fd, const void *buf, size_t len, int flags);

關閉 socket 執行個體
int nty_close(int fd);
           

【2.2】協程的實作

1. 協程的實作之建立協程

int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg);
           
  • 參數 1 : nty_coroutine **new_co,需要傳入空的協程的對象,這個對象是由函數内部建立并傳回的;
  • 參數 2 : proc_coroutine func 協程的子過程;當協程被排程的時候,就會執行該函數;
  • 參數 3 : void *arg 需要傳入到新協程中的參數;

協程不存在親屬關系,都是一緻的排程關系,接受排程器的排程;調用 create API 就會建立一個新協程,新協程就會加入到排程器的就緒隊列中;

2. 協程的實作之實作 IO 異步操作

排程器與協程的上下文切換圖示

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

在協程的上下文 IO 異步操作函數,步驟如下

  • 1. 将 sockfd 添加到 epoll 管理中
  • 2. 進行上下文環境切換,由協程上下文 yield 到排程器的上下文
  • 3. 排程器擷取下一個協程上下文,resume 新的協程

IO 異步操作的上下文切換的時序圖

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

3. 協程的實作之回調協程的子過程

關鍵示例代碼

static void nty_coroutine_init(nty_coroutine *co) {

	void **stack = (void **)(co->stack + co->stack_size);

	stack[-3] = NULL;
	stack[-2] = (void *)co;

	// ctx 協程的上下文
	// 可以将回調函數位址存入 EIP 中,将相應參數存儲到相應的參數寄存器中
	co->ctx.esp = (void*)stack - (4 * sizeof(void*));
	co->ctx.ebp = (void*)stack - (3 * sizeof(void*));
	// CPU 寄存器 EIP 用于存儲 CPU 運作下一條指令的位址
	co->ctx.eip = (void*)_exec;	// 設定回調函數入口
	co->status = BIT(NTY_COROUTINE_STATUS_READY);
	
}
           
static void _exec(void *lt) {
#if defined(__lvm__) && defined(__x86_64__)
	__asm__("movq 16(%%rbp), %[lt]" : [lt] "=r" (lt));
#endif

	nty_coroutine *co = (nty_coroutine*)lt;
	co->func(co->arg); //
	co->status |= (BIT(NTY_COROUTINE_STATUS_EXITED) | BIT(NTY_COROUTINE_STATUS_FDEOF) 
		| BIT(NTY_COROUTINE_STATUS_DETACH));
	
#if 1
	nty_coroutine_yield(co);
#else
	co->ops = 0;
	_switch(&co->sched->ctx, &co->ctx);
#endif
}
           

4. 協程實作之原語操作

協程的核心原語操作 : create, resume, yield;

協程的原語操作有 create 但卻沒有 exit 的分析

  • 以 NtyCo 為例,協程一旦建立就不能由使用者自己銷毀,必須在子過程執行結束時便自動銷毀協程的上下文資料;
  • 以 _exec 執行入口函數傳回而銷毀協程的上下文與相關資訊;
  • co->func(co->arg) 是子過程,若使用者需要長久運作協程,就必須要在 func 函數裡面寫入循環等操作;
    • 是以 NtyCo 裡面沒有實作 exit 的原語操作

原語 create : 建立一個協程

// 1. 排程器是否存在,不存在也建立;排程器作為全局的單例;将排程器的執行個體存儲線上程的私有空間 pthread_setspecific
// 2. 配置設定一個 coroutine 的記憶體空間,分别設定 
// 		coroutine 的資料項,棧空間,棧大小,初始狀态,建立時間,子過程回調函數,子過程的調用參數
// 3. 将新配置設定協程添加到就緒隊列 ready_queue 中

int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg)
           

原語 yield : 讓出 CPU

// 參數:需要恢複運作的協程執行個體
// 	    調用後該函數也不會立即傳回,而是切換到運作協程執行個體的 yield 的位置;
// 		傳回是在等協程相應事務處理完成後,主動 yield 會傳回到 resume 的地方;

void nty_coroutine_yield(nty_coroutine *co)
           

原語 resume : 恢複協程的運作權

// 參數:需要恢複運作的協程執行個體
//		調用後該函數也不會立即傳回,而是切換到運作協程執行個體的 yield 的位置;
//		傳回是在等協程相應事務處理完成後,主動 yield 會傳回到 resume 的地方;

int nty_coroutine_resume(nty_coroutine *co)
           

5. 協程實作之切換

上下文切換,就是将 CPU 的寄存器暫時儲存,再将即将運作的協程的上下文寄存器,分别 mov 到相對應的寄存器上,進而完成上下文的切換;

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

nty_cpu_ctx 結構體

// 關聯相關寄存器的結構體
typedef struct _nty_cpu_ctx {
	void *esp;
	void *ebp;
	void *eip;
	void *edi;
	void *esi;
	void *ebx;
	void *r1;
	void *r2;
	void *r3;
	void *r4;
	void *r5;
} nty_cpu_ctx;
           

_switch 函數 API

// 參數 1 : 即将運作協程的上下文,寄存器清單
// 參數 2 : 正在運作協程的上下文,寄存器清單
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
// _switch 傳回後,執行即将運作協程的上下文,進而實作上下文的切換
           

_switch 函數實作代碼分析

// %rdi 函數第一個參數, %rsi 函數第二個參數
__asm__ (
"    .text                                  \n"
"       .p2align 4,,15                                   \n"
".globl _switch                                          \n"
".globl __switch                                         \n"
"_switch:                                                \n"
"__switch:                                               \n"
"       movq %rsp, 0(%rsi)      # save stack_pointer     \n"    // 儲存棧指針到 cur_ctx 執行個體的 rsp 項
"       movq %rbp, 8(%rsi)      # save frame_pointer     \n"    // 儲存幀指針到 cur_ctx 執行個體的 rbp 項
"       movq (%rsp), %rax       # save insn_pointer      \n"    // 将棧頂位址裡面的值存儲到 rax 寄存器中
"       movq %rax, 16(%rsi)                              \n"    // 儲存 rax,rbx,r12 - r15
"       movq %rbx, 24(%rsi)     # save rbx,r12-r15       \n"
"       movq %r12, 32(%rsi)                              \n"
"       movq %r13, 40(%rsi)                              \n"
"       movq %r14, 48(%rsi)                              \n"
"       movq %r15, 56(%rsi)                              \n"
"       movq 56(%rdi), %r15                              \n"    // 将 rdi 對應偏移量中的值儲存入相應的寄存器中
"       movq 48(%rdi), %r14                              \n"    // 即将 rdi 對應偏移量中的值儲存入 new_ctx 的每一個項中
"       movq 40(%rdi), %r13     # restore rbx,r12-r15    \n"
"       movq 32(%rdi), %r12                              \n"
"       movq 24(%rdi), %rbx                              \n"
"       movq 8(%rdi), %rbp      # restore frame_pointer  \n"
"       movq 0(%rdi), %rsp      # restore stack_pointer  \n"
"       movq 16(%rdi), %rax     # restore insn_pointer   \n"    // 将指令指針 rip 的值存儲到 rax 中
"       movq %rax, (%rsp)                                \n"    // 将存儲 rip 值的 rax 寄存器指派給棧指針指向的位址處
"       ret                                              \n"    // 出棧,回到棧指針,執行 rip 指向的指令
);
           

6. 協程實作之定義

協程的運作體 R 與運作體排程器 S

  • 1. 運作體 R:包含運作狀态 { 就緒,睡眠,等待 },運作體回調函數,回調參數,棧指針,棧大小,目前運作體;
  • 2. 排程器 S:包含執行集合 { 就緒,睡眠,等待 };

6.1 運作體在多種狀态集合的切換

協程的狀态

  • 建立新的協程,建立完成後,便加入到就緒集合,等待排程器的排程; 
  • 協程在運作完成後,進行 IO 操作,此時 IO 并未準備好,便進入等待狀态集合;
  • IO 準備就緒,協程開始運作,後續進行 sleep 操作,便進入到睡眠狀态集合;

就緒 (ready),睡眠 (sleep),等待 (wait) 集合對應的資料結構

  • 就緒 (ready) 集合并沒有設定優先級的選型,所有的協程優先級一緻,可以使用隊列來存儲就緒的協程,簡稱為就緒隊列 (ready_queue)
  • 睡眠 (sleep) 集合需要按照睡眠時長進行排序,是以采用紅黑樹來存儲,簡稱睡眠樹 (sleep_tree);紅黑樹在工程中為 <key, value>, key 為睡眠時長,value 為對應的協程結點
  • 等待 (wait) 集合,其功能是在等待 IO 準備就緒,等待 IO 是有時長的,是以等待 (wait) 集合采用紅黑樹存儲,簡稱等待樹 (wait_tree)
【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

圖示中,Coroutine 是協程的相應屬性,status 表示協程的運作狀态,并包含 sleep 與 wait 兩棵紅黑樹以及 ready 隊列;前提條件是不管何種運作狀态的協程,都在就緒隊列中,隻是同時包含有其他的運作狀态;

6.2 排程器與協程的功能界限

協程屬性 : 每一協程都需要使用的而且可能會不同的屬性;排程器屬性 : 每一協程都需要的而且一緻的資料;

協程的核心結構體

typedef struct _nty_coroutine {

	//private
	// 協程自身的上下文,需要儲存 CPU 的寄存器 ctx
	nty_cpu_ctx ctx;
	// 子過程的回調函數 func
	proc_coroutine func; 
	// 子過程回調函數的參數 arg 以及資料 data
	void *arg;
	void *data;
	// 協程自身的棧空間的大小 stack_size
	size_t stack_size;
	size_t last_stack_size;
	// 協程目前的運作狀态
	nty_coroutine_status status;
	// 排程器的全局對象
	nty_schedule *sched;
	// 協程的建立時間
	uint64_t birth;
	// 協程 id
	uint64_t id;
#if CANCEL_FD_WAIT_UINT64
	int fd;
	unsigned short events;  //POLL_EVENT
#else
	int64_t fd_wait;
#endif
	char funcname[64];
	struct _nty_coroutine *co_join;

	void **co_exit_ptr;
	// 協程自身的棧空間
	void *stack;
	void *ebp;
	uint32_t ops;
	uint64_t sleep_usecs;

	// 目前運作狀态的結點
	RB_ENTRY(_nty_coroutine) sleep_node;
	RB_ENTRY(_nty_coroutine) wait_node;

	LIST_ENTRY(_nty_coroutine) busy_next; //

	TAILQ_ENTRY(_nty_coroutine) ready_next;
	TAILQ_ENTRY(_nty_coroutine) defer_next;
	TAILQ_ENTRY(_nty_coroutine) cond_next;

	TAILQ_ENTRY(_nty_coroutine) io_next;
	TAILQ_ENTRY(_nty_coroutine) compute_next;

	struct {
		void *buf;
		size_t nbytes;
		int fd;
		int ret;
		int err;
	} io;

	struct _nty_coroutine_compute_sched *compute_sched;
	int ready_fds;
	struct pollfd *pfds;
	nfds_t nfds;
} nty_coroutine;
           

排程器的屬性,需要有儲存 CPU 的寄存器上下文 ctx,可以從協程運作狀态 yield 到排程器運作;從協程到排程器用 yield,從排程器到協程用 resume;

排程器結構體

typedef struct _nty_coroutine_link nty_coroutine_link;
typedef struct _nty_coroutine_queue nty_coroutine_queue;

typedef struct _nty_coroutine_rbtree_sleep nty_coroutine_rbtree_sleep;
typedef struct _nty_coroutine_rbtree_wait nty_coroutine_rbtree_wait;

typedef struct _nty_schedule {
	uint64_t birth;
	nty_cpu_ctx ctx;
	void *stack;
	size_t stack_size;
	int spawned_coroutines;
	uint64_t default_timeout;
	struct _nty_coroutine *curr_thread;
	int page_size;

	int poller_fd;
	int eventfd;
	struct epoll_event eventlist[NTY_CO_MAX_EVENTS];
	int nevents;

	int num_new_events;
	pthread_mutex_t defer_mutex;

	nty_coroutine_queue ready;
	nty_coroutine_queue defer;

	nty_coroutine_link busy;
	
	nty_coroutine_rbtree_sleep sleeping;
	nty_coroutine_rbtree_wait waiting;

	//private 

} nty_schedule;
           

7. 協程實作之排程器

排程器的實作,有兩種方案,一種是生産者消費者模式,另一種是多狀态運作;

7.1 生産者消費者模式

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

邏輯代碼示例

while (1) {
    // 周遊睡眠集合,将滿足條件的協程加入到 ready 隊列
    nty_coroutine *expired = NULL;
    while ((expired = sleep_tree_expired(sched)) != NULL) {
        TAILQ_ADD(&sched->ready, expired);
    }
    // 周遊等待集合,将滿足條件的協程加入到 ready 隊列
    nty_coroutine *wait = NULL;
    int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1);
    for (i = 0; i < nready; i ++) {
        wait = wait_tree_search(events[i].data.fd);
        TAILQ_ADD(&sched->ready, wait);
    }
    // 使用 resume 恢複 ready 的協程運作權
    while (!TAILQ_EMPTY(&sched->ready)) {
        nty_coroutine *ready = TAILQ_POP(sched->ready);
        resume(ready);
    }
}
           

7.2 多狀态運作

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

邏輯代碼示例

while (1) {
    // 周遊睡眠集合,使用 resume 恢複 expired 的協程運作權
    nty_coroutine *expired = NULL;
    while ((expired = sleep_tree_expired(sched)) != NULL) {
        resume(expired);
    }
    // 周遊等待集合,使用 resume 恢複 wait 的協程運作權
    nty_coroutine *wait = NULL;
    int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1);
    for (i = 0; i < nready; i ++) {
        wait = wait_tree_search(events[i].data.fd);
        resume(wait);
    }
    // 使用 resume 恢複 ready 的協程運作權
    while (!TAILQ_EMPTY(sched->ready)) {
        nty_coroutine *ready = TAILQ_POP(sched->ready);
        resume(ready);
    }
}
           

補充知識點

1. X86-64寄存器和棧幀

1.1 寄存器

通用寄存器

X86_64 有 16 個 64 位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15;

  • %rax 作為函數傳回值使用
  • %rsp 棧指針寄存器,指向棧頂
  • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函數參數,依次對應第 1 參數,第 2 參數...
  • %rbx,%rbp,%r12,%r13,%r14,%r15 用作資料存儲,遵循被調用者使用規則,調用子函數之前要備份上述寄存器,以防被修改
  • %r10,%r11 用作資料存儲,遵循調用者使用規則,即使用之前要先儲存原值
【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

段寄存器

  • cs 代碼段寄存器
  • ds, es, fs, gs 資料段寄存器
  • ss 堆棧段寄存器

狀态和控制寄存器 eflags

該寄存器表示的意義非常豐富,程式中并不直接操作此寄存器,并由此衍生出很多操作指令,除去一些保留位,其他每位都代表一個具體的含義,

其中 bits 0, 2, 4, 6, 7, 11 是狀态位,辨別了此操作後的狀态

  • CF (bit 0) —— 進位辨別,算術操作進行了進位和借位,則此位被設定
  • PF (bit 2) —— 奇偶辨別,結果包含奇數個1,則設定此位
  • AF (bit 4) —— 輔助進位辨別,結果的第3位向第4位借位,則此位被設定
  • ZF (bit 6) —— 零辨別,結果為零,此位設定
  • SF (bit 7) —— 符号辨別,若為負數則設定此位
  • OF (bit 11) —— 溢出辨別,結果向最高位符号位進行借位或者進位,此标志被設定

8, 9, 10 位為控制辨別

  • TF (bit 8) —— 陷阱辨別,設定程序可以被單步調試
  • IF (bit 9) —— 中斷辨別,設定能夠響應中斷請求
  • DF (bit 10) —— 方向辨別,用于标示字元處理過程中指針移動方向

指令寄存器 EIP

EIP —— 标志目前程序将要執行指令位置,在64位模式下擴充為 RIP 64位指令寄存器

1.2 棧幀

1.2.1 棧幀結構

C 語言屬于面向過程語言,其最大特點就是把一個程式分解成若幹過程 (函數),比如:入口函數是 main,然後調用各個子函數;在對應機器語言中,GCC 把過程轉化成棧幀 (frame),每個棧幀對應一個過程;X86-32 典型棧幀結構中,由 %ebp 指向棧幀開始,%esp 指向棧頂;

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

1.2.2 C 函數與彙編分析

int foo ( int x )
{
    int array[] = {1,3,5};
    return array[x];
}   /* -----  end of function foo  ----- */

int main ( int argc, char *argv[] )
{
    int i = 1;
    int j = foo(i);
    fprintf(stdout, "i=%d,j=%d\n", i, j);
    return EXIT_SUCCESS;
}   /* -----  end of function main  ----- */
           

 未經過優化的結果

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

優化後的結果

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

從優化可見,此處直接引用了棧頂之外的空間;

通路棧頂之外

通過 readelf 檢視可執行程式的 header 資訊

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

根據紅色區域可見 x86-64 遵循 ABI 規則的版本,其定義了一些規範,遵循 ABI 的具體實作應該滿足這些規範,其中,便規定了程式可以使用棧頂之外 128 位元組的位址;

彙編相關分析

一、movl value, %eax

  • movl value, %eax 這條指令是把 value 的值 (32位),送入 %eax 寄存器;
  • value 表達式格式 : base_address (offset_address, index, size);計算公式 : base_address + offset_address + index * size;

二、call foo

  • Pushl %rip // 儲存下一條指令的位址,用于函數傳回繼續執行
  • Jmp foo     // 跳轉到函數 foo

三、ret

  • popl %rip // 恢複指令指針寄存器

四、cltq

  • cltq 指令,特指 %eax->%rax 的符号拓展轉換,等價于 movslq %eax, %rax

五、.LC0

.LC0 指一個字元串,此處代表 fprintf 的格式化字元串;

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

六、leave

leave 指令等價于

Movq %rbp %rsp 	// 撤銷棧空間,復原 %rsp
Popq %rbp 		// 恢複上一個棧幀的 %rbp
           

1.3 寄存器儲存慣例

C 語言示例代碼

#include <stdio.h>
#include <stdlib.h>

void sfact_helper ( long int x, long int * resultp)
{
    if (x <= 1)
       *resultp = 1;
    else {
       long int nresult;
       sfact_helper(x-1, &nresult);
       *resultp = x * nresult;
    }
}      /* -----  end of function foo  ----- */

long int sfact ( long int x )
{
    long int result;
    sfact_helper(x, &result);
    return result;
}      /* -----  end of function sfact  ----- */

int
main ( int argc, char *argv[] )
{
    int sum = sfact(10);
    fprintf(stdout, "sum=%d\n", sum);
    return EXIT_SUCCESS;
}     /* ----------  end of function main  ---------- */
           

sfact_helper 函數生成的彙編代碼

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

在函數 sfact_helper 中,用到了寄存器 %rbx 和 %rbp,在覆寫之前,GCC 選擇了先儲存他們的值,如代碼 6~9 所示;在函數傳回之前,GCC 依次恢複了他們,如代碼 27-28 所示;

%rbx 在函數進入的時候指向的是 -16(%rsp),而在退出的時候,指向的是32(%rsp);此處使用了通路棧幀之外的空間的特性,即 GCC 不用預先配置設定空間再使用,而是先使用棧空間,然後在适當的時機配置設定,如第 11 行代碼配置設定了空間,之後棧指針發生變化,是以同一個位址的引用偏移也相應做出調整;

注意事項

  • 盡量使用 6 個以下的參數清單
  • 傳遞大對象,盡量使用指針或者引用,鑒于寄存器隻有 64 位,而且隻能存儲整形數值,寄存器存不下大對象

彙編相關分析

一、lea 指令

  • 指令格式 : LEA 目的 源
  • 指令功能 : 取源操作數位址的偏移量,并把它傳送到目的操作數所在的單元

二、imul 指令

  • (1) 雙操作數的有符号乘指令
    • 語句格式 : IMUL OPD, OPS
    • 功能 : (OPD) * (OPS) ----> OPD
    • 其中 OPD 可為 16/32 的寄存器,OPS 可為同類型的寄存器、存儲器操作數或立即數
  • (2) 3 個操作數的有符号乘指令
    • 語句格式 : IMUL OPD, OPS, N
    • 功能 : (OPS * N) -----> OPD
    • 其中 OPD 可為 16/32 的寄存器,OPS 可為同類型的寄存器、存儲器操作數,n 為立即數
  • (3) 單操作數的有符号乘指令
    • 語句格式 : IMUL OPS
    • 功能 :
      • 位元組乘法 : (AL) * (OPS) ----> AX
      • 字乘法 : (AX) * (OPS) ----> DX, AX
      • 雙字乘法 : (EAX) * (OPS) ----> EDX, EAX

三、mul 指令

  • 語句格式 : MUL OPS
    • 功能 :
      • 位元組乘法 : (AL) * (OPS) ----> AX
      • 字乘法 : (AX) * (OPS) ----> DX, AX
      • 雙字乘法 : (EAX) * (OPS) ----> EDX, EAX

1.4 參數傳遞範例

C 示例代碼

#include <stdio.h>
#include <stdlib.h>

int foo ( int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7 )
{
    int array[] = {100, 200, 300, 400, 500, 600, 700};
    int sum = array[ arg1 ] + array[ arg7 ];
    return sum;
}       /* -----  end of function foo  ----- */

int
main ( int argc, char *argv[] )
{
    int i = 1;
    int j = foo(0, 1, 2, 3, 4, 5, 6);
    fprintf(stdout, "i = %d, j = %d\n", i, j);
    return EXIT_SUCCESS;
}       /* ----------  end of function main  ---------- */
           

生成的彙編代碼

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)

Main 函數中,代碼 31~37 準備函數 foo 的參數

  • 參數 7 存儲在棧上即 %rsp 指向的位置;
  • 參數 6 存儲在寄存器 %r9d;
  • 參數 5 存儲在寄存器 %r8d;
  • 參數 4 對應于 %ecx;
  • 參數 3 對應于 %edx;
  • 參數 2 對應于 %esi;
  • 參數 1 對應于 %edi;

Foo 函數中,代碼 14-15,分别取出參數 7 和參數 1,參與運算;

此處數組引用,使用了最經典的尋址方式,-40(%rsp,%rdi,4) = %rsp + %rdi * 4 + (-40); 其中 %rsp 用作數組基位址,%rdi 用作了數組的下标,數字 4 表示 sizeof(int) = 4

1.5 結構體傳參

C 示例代碼

#include <stdio.h>
#include <stdlib.h>

struct demo_s {
    char var8;
    int  var32;
    long var64;
};

struct demo_s foo (struct demo_s d)
{
    d.var8 = 8;
    d.var32 = 32;
    d.var64 = 64;
    return d;
}       /* -----  end of function foo  ----- */

int
main ( int argc, char *argv[] )
{
    struct demo_s d, result;
    result = foo (d);
    fprintf(stdout, "demo: %d, %d, %ld\n", result.var8, result.var32, result.var64);
    return EXIT_SUCCESS;
}       /* ----------  end of function main  ---------- */
           

生成彙編代碼

【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)【網絡通信 -- 直播】SRS 源碼分析 -- 協程基礎知識點總結(一)
  • 問題1 : 結構體如何傳遞;被分成了兩個部分,var8 和 var32 合并成 8 個位元組的大小,放在寄存器 %rdi 中,var64 放在寄存器的 %rsi 中,即結構體分解
  • 問題2 : 結構體如何存儲;如 foo 函數的第 15~17 行所示,結構體的引用變成了一個偏移量通路;類似于數組,隻不過其元素大小可變;
  • 問題3 : 結構體如何傳回,原本 %rax 充當傳回值的角色,現在添加第二個傳回值角色 %rdx;同樣,GCC 用兩個寄存器來表示結構體;
    • 即使在預設情況下,GCC 依然想盡辦法使用寄存器;随着結構變得越來越大,寄存器不夠用了,則隻能使用棧了;

參考與緻謝

本部落格為部落客的學習實踐總結,并參考了衆多部落客的博文,在此表示感謝,部落客若有不足之處,請批評指正。

【1】協程的實作與原理 [ M ]

【2】X86-64寄存器和棧幀

【3】x64 ASM 常用彙編指令

【4】movl 16(%ebx,%esi,4),%eax的含義是?

【5】寄存器介紹

【6】Push, Pop, call, leave 和 Ret 指令圖解

【7】鎮宅之寶扛鼎之作

【8】協程的實作之切換