首頁 > 軟體

解析go語言呼叫約定多返回值實現原理

2022-05-24 22:04:23

go簡單程式碼反組合

用簡單的程式碼用以分析go的呼叫約定及多返回值的返回方式。

package main
func vals(c, d int) (a int, b int) {
	e := 1
	f := 2
	a = c + d + e + f
	b = d * 2
	return
}
func testMutil() {
	i, j := vals(1, 2)
	i = i + 1
	j = j + 1
}
func main() {
	testMutil()
}

編譯go build -gcflags "-N -l" test.go並通過反編譯軟體獲得部分組合:

main_vals:
    sub     rsp, 18h
    mov     [rsp+18h+var_8], rbp
    lea     rbp, [rsp+18h+var_8]
    mov     [rsp+18h+arg_10], 0
    mov     [rsp+18h+arg_18], 0
    mov     [rsp+18h+var_10], 1
    mov     [rsp+18h+var_18], 2
    mov     rax, [rsp+18h+arg_0]
    add     rax, [rsp+18h+arg_8]
    add     rax, [rsp+18h+var_10]
    add     rax, 2
    mov     [rsp+18h+arg_10], rax
    mov     rax, [rsp+18h+arg_8]
    shl     rax, 1
    mov     [rsp+18h+arg_18], rax
    mov     rbp, [rsp+18h+var_8]
    add     rsp, 18h
    retn
main_vals endp
main_testMutil:
    mov     rcx, gs:28h
    mov     rcx, [rcx+0]
    cmp     rsp, [rcx+10h]
    jbe     short morestack_noctxt
    sub     rsp, 48h
    mov     [rsp+48h+var_8], rbp
    lea     rbp, [rsp+48h+var_8]
    mov     [rsp+48h+var_48], 1
    mov     [rsp+48h+var_40], 2
    call    main_vals
    mov     rax, [rsp+48h+var_38]
    mov     [rsp+48h+var_10], rax
    mov     rax, [rsp+48h+var_30]
    mov     [rsp+48h+var_18], rax
    mov     rax, [rsp+48h+var_10]
    mov     [rsp+48h+var_20], rax
    mov     rax, [rsp+48h+var_18]
    mov     [rsp+48h+var_28], rax
    mov     rax, [rsp+48h+var_20]
    inc     rax
    mov     [rsp+48h+var_20], rax
    mov     rax, [rsp+48h+var_28]
    inc     rax
    mov     [rsp+48h+var_28], rax
    mov     rbp, [rsp+48h+var_8]
    add     rsp, 48h
    retn
morestack_noctxt:
    call    runtime_morestack_noctxt
main_testMutil endp

go語言呼叫約定分析

1.C/C++呼叫約定類別

__stdcall呼叫約定:函數的引數自右向左通過棧傳遞,被呼叫的函數在返回前清理傳送引數的記憶體棧。

_cdecl是C和C++程式的預設呼叫方式。每一個呼叫它的函數都包含清空堆疊的程式碼,所以產生的可執行檔案大小會比呼叫_stdcall函數的大。函數採用從右到左的壓棧方式。注意:對於可變引數的成員函數,始終使用__cdecl的轉換方式。

__fastcall呼叫約定:它是通過暫存器來傳送引數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的引數,剩下的引數仍舊自右向左壓棧傳送,被呼叫的函數在返回前清理傳送引數的記憶體棧)。

thiscall僅僅應用於”C++”成員函數。this指標存放於CX暫存器,引數從右到左壓。thiscall不是關鍵詞,因此不能被程式設計師指定。

naked call採用1-4的呼叫約定時,如果必要的話,進入函數時編譯器會產生程式碼來儲存ESI,EDI,EBX,EBP暫存器,退出函數時則產生程式碼恢復這些暫存器的內容。naked call不產生這樣的程式碼。naked call不是型別修飾符,故必須和_declspec共同使用。

2.go語言呼叫約定

sub     rsp, 18h
mov     [rsp+18h+var_8], rbp
...
mov     rbp, [rsp+18h+var_8]
add     rsp, 18h

這段程式碼分別對應棧幀的構造與銷燬。
根據反組合並且偵錯,可以發現go語言引數是自右向左通過棧傳遞,被呼叫的函數在返回前清理傳送引數的記憶體棧。所以GO語言符合__stdcall呼叫約定。

go語言如何實現多返回值的

go語言可以返回多個返回值, 但同為編譯型語言的C、C++卻不支援。

1.C/C++返回值返回方式。

C/C++是通過eax/rax(32/64bit)暫存器返回的返回值。

2.go語言多返回值返回方式

可以看到vals函數的組合,通過偵錯,可知arg_10與arg_18就是返回值a和b, arg_0與arg_8分別是引數c和d。其中

mov     [rsp+18h+arg_10], rax
...
mov     [rsp+18h+arg_18], rax

分別將引數值返回到引數上。之後在main_testMutil中將引數返回值拷貝到對應區域性變數中

mov     rax, [rsp+48h+var_38]
mov     [rsp+48h+var_10], rax
mov     rax, [rsp+48h+var_30]
mov     [rsp+48h+var_18], rax

這就是go語言多返回值的實現方法了。

總結

go語言採用的是__stdcall呼叫約定。go多返回值是通過棧傳遞的。將多個返回值先傳回引數上,函數棧幀銷燬後並不會銷燬引數部分(這裡用作返回值),再將引數部分進行拷貝然後再參與運算。

以上就是解析go語言呼叫約定多返回值實現原理的詳細內容,更多關於go呼叫約定多返回值的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com