1. 程式人生 > >從匯編層面看函數調用的實現原理

從匯編層面看函數調用的實現原理

ros 操作 作用 成了 關註 ofa 發生 狀態 ini

本文是《go調度器源代碼情景分析》系列 第一章 預備知識的第6小節。

前面幾節我們介紹了CPU寄存器、內存、匯編指令以及棧等基礎知識,為了達到融會貫通加深理解的目的,這一節我們來綜合運用一下前面所學的這些知識,看看函數的執行和調用過程。

本節我們需要重點關註的問題有:

  • CPU是如何從調用者跳轉到被調用函數執行的?

  • 參數是如何從調用者傳遞給被調用函數的?

  • 函數局部變量所占內存是怎麽在棧上分配的?

  • 返回值是如何從被調用函數返回給調用者的?

  • 函數執行完成之後又需要做哪些清理工作?

解決了這些問題,我們對計算機執行程序的原理就會有一個大致的了解,這對於我們理解goroutine的調度有非常重要的作用。

相對於go來說,C語言更接近於硬件,編譯後的匯編代碼也更加簡單直觀,更容易讓我們掌握函數調用的基本原理,所以我們首先來看C語言的函數調用在匯編指令層面是如何實現的,然後在此基礎上分析go語言的函數調用過程。

C語言函數調用過程

我們用一個簡單的例子程序來開始分析。

#include <stdio.h>

// 對參數 a 和 b 求和
intsum(inta, intb)
{
        ints=a+b;

        returns;
}

// main函數:程序入口
intmain(intargc, char*argv[])
{
        intn
=sum(1, 2); // 調用sum函數對求和 printf("n: %d\n", n); //在屏幕輸出 n 的值 return0; }

用gcc編譯這個程序得到可執行程序call,然後使用gdb調試。在gdb中我們通過disass main反匯編main函數找到main的第一條指令所在的地址為0x0000000000400540,然後使用b *0x0000000000400540在該地址下一個斷點並運行程序:

[email protected]:~/study/c$ gdb ./call
(gdb) disass main
Dumpofassemblercodeforfunctionmain:
0x0000000000400540<+0>:push %rbp 0x0000000000400541<+1>:mov %rsp,%rbp 0x0000000000400544<+4>:sub $0x20,%rsp 0x0000000000400548<+8>:mov %edi,-0x14(%rbp) 0x000000000040054b<+11>:mov %rsi,-0x20(%rbp) 0x000000000040054f<+15>:mov $0x2,%esi 0x0000000000400554<+20>:mov $0x1,%edi 0x0000000000400559<+25>:callq 0x400526 <sum> 0x000000000040055e<+30>:mov %eax,-0x4(%rbp) 0x0000000000400561<+33>:mov -0x4(%rbp),%eax 0x0000000000400564<+36>:mov %eax,%esi 0x0000000000400566<+38>:mov $0x400604,%edi 0x000000000040056b<+43>:mov $0x0,%eax 0x0000000000400570<+48>:callq 0x400400 <[email protected]> 0x0000000000400575<+53>:mov $0x0,%eax 0x000000000040057a<+58>:leaveq 0x000000000040057b<+59>:retq Endofassemblerdump. (gdb) b *0x0000000000400540 Breakpoint1at0x400540 (gdb) r Startingprogram:/home/bobo/study/c/call Breakpoint1, 0x0000000000400540 in main ()

程序停在了我們下的斷點處,也就是main函數的第一條指令的位置。再次反匯編一下將要執行的main函數,我們先來看其最前面的3條指令:

(gdb) disass
Dumpofassemblercodeforfunctionmain:
=> 0x0000000000400540 <+0>:push   %rbp
  0x0000000000400541<+1>:mov   %rsp,%rbp
  0x0000000000400544<+4>:sub   $0x20,%rsp
  ......

這3條指令我們一般稱之為函數序言,基本上每個函數都以函數序言開始,其主要作用在於保存調用者的rbp寄存器以及為當前函數分配棧空間,後面我們會詳細介紹這3條指令,我們先來說明一下gdb輸出的反匯編代碼的格式,gdb反匯編出來的代碼主要分為3個部分:

  • 指令地址

  • 指令相對於當前函數起始地址以字節為單位的偏移

  • 指令

比如第一行代碼 0x0000000000400540 <+0>: push %rbp,表示main函數的第一條指令push %rbp在內存中的地址為0x0000000000400540,偏移為0(因為它是main函數的第一條指令)。這行代碼各組成部分如下圖所示:

技術分享圖片

這裏需要說明一點,gdb反匯編輸出的結果中的指令地址和偏移只是gdb為了讓我們更容易閱讀代碼而附加上去的,保存在內存中以及被CPU執行的代碼只有上圖指令部分。

註意,上面反匯編結果中的第一行代碼的最左邊還有一個 => 符號,它表示這條指令是CPU將要執行的下一條指令,也就是rip寄存器目前的值為0x0000000000400540,當前的狀態是前一條指令已經執行完畢,這一條指令還未開始執行,使用i r rbp rsp rip察看一下rbp、rsp和rip這3個寄存器的值:

(gdb) i r rbprsprip
rbp          0x4005800x400580 <__libc_csu_init>
rsp          0x7fffffffe5180x7fffffffe518
rip          0x4005400x400540 <main>

根據這些寄存器的值,當前時刻函數調用棧、rbp、 rsp和rip的狀態以及它們之間的關系如下圖所示:

技術分享圖片

因為rbp、rsp和rip存放的都是地址,所以這幾個寄存器每個都相當於一個指針,看上圖,rip指向的是main函數的第一條指令,rsp指向了當前函數調用棧的棧頂,而rbp寄存器並未指向我們關註的棧和指令,於是並未畫出它的具體指向,只是顯示了它的值。

為了更加清晰的理解程序的執行流程,現在我們開始模擬CPU從main函數的第一條指令開始,一直到執行完整個main函數。

現在開始執行第1條指令,

0x0000000000400540<+0>:push   %rbp# 保存調用者的rbp寄存器的值

這條指令把棧基地址寄存器rbp的值臨時保存在main函數的棧幀裏,因為main函數需要使用這個寄存器來存放自己的棧基地址,而調用者在調用main函數之前也可能把它的棧基地址保存在了這個寄存器裏面或用作了其它用途,所以main函數需要把這個寄存器裏面的值先保存起來,等main執行完後返回時再把這個寄存器恢復原樣,如果不恢復原樣,main函數返回後調用者使用rbp寄存器時就會有問題,因為在執行調用者的代碼時rbp本應該指向調用者的棧但現在卻指向了main函數的棧。

在這條指令之前,代碼還在使用調用者的棧幀,執行完這條指令之後,開始使用main函數的棧幀,目前main函數的棧幀裏面只保存有調用者的rbp這一個值,在繼續執行下一條指令之前,棧和寄存器的狀態如下圖,圖中標紅的指令表示剛執行完成的指令。可以看到rsp和rip寄存器的值都已經發生了改變,它們都指向了新的位置。rsp指向了main函數的棧幀的起始位置,而rip指向了main函數的第2條指令。

技術分享圖片

在匯編指令一節我們介紹過,執行push指令會修改rsp寄存器的值,但它並不會修改rip寄存器,為什麽這裏rip也變了呢?其實這是CPU自動完成的,CPU自己知道它要執行的每一條指令的長度有幾個字節,比如這裏的push %rbp指令只有1個字節長,於是它在開始執行這條指令時就會把rip的值+1,因為執行這條指令之前rip的值為0x400540,+1之後就變成了0x400541,也就是說它指向了main函數的第2條指令。

接著執行第2條指令,

0x0000000000400541<+1>:mov   %rsp,%rbp# 調整rbp寄存器,使其指向main函數棧幀的起始位置

這條指令把rsp的值拷貝給rbp寄存器,讓其指向main函數棧幀的起始位置,執行完這條指令之後rsp和rbp寄存器具有相同的值,他們都指向了main函數的棧幀的起始位置,如下圖所示:

技術分享圖片

接著執行第3條指令,

0x0000000000400544<+4>:sub   $0x20,%rsp# 調整rsp寄存器的值,為局部和臨時變量預留棧空間

這條指令把rsp寄存器的值減去了32(16進制的0x20),使其指向了棧空間中一個更低的位置,這一步看似只是簡單的修改了rsp寄存器的值,其實質卻是給main函數的局部變量和臨時變量預留了32(0x20)字節的棧空間,為什麽說是預留而不是分配,因為棧的分配是操作系統自動完成的,程序啟動時操作系統就會給我們分配一大塊內存用作函數調用棧,程序到底使用了多少棧內存由rsp棧頂寄存器來確定。

該指令執行完成之後,從rsp所指位置到rbp所指的這一段棧內存就構成了main函數的完整棧幀,其大小為40字節(8字節用於保存調用者的rbp,另外32字節用於main函數的局部和臨時變量),如下圖:

技術分享圖片

接下來的4條指令我們一起把它們執行了,

0x0000000000400548<+8>:mov   %edi,-0x14(%rbp)
0x000000000040054b<+11>:mov   %rsi,-0x20(%rbp)
0x000000000040054f<+15>:mov   $0x2,%esi#sum函數的第2個參數放入esi寄存器
0x0000000000400554<+20>:mov   $0x1,%edi#sum函數的第1個參數放入edi寄存器

前兩條指令負責把main函數得到的兩個參數保存在main函數的棧幀裏面,可以看到,這裏使用了rbp加偏移量的方式來訪問棧內存。這裏之所以要保存main函數的兩個參數,是因為調用者在調用main函數時使用了edi和rsi兩個寄存器來給main函數分別傳遞argc(整數)和argv(指針)兩個參數,而main又需要用這兩個寄存器給sum函數傳遞參數,為了不覆蓋argc和argv,所以這裏需要先把這兩個參數保存在棧裏面,然後再把傳遞給sum函數的兩個參數1和2放入這兩個寄存器之中。

後面兩條指令在給sum函數準備參數,我們可以看到,傳遞給sum的第一個參數放在了edi寄存器裏面,第二個參數放在了esi裏面。可能你會問,被調用函數怎麽知道參數放在這兩個寄存器裏面的啊?其實這是一個約定而已,大家約定好:調用函數時調用者負責把第一個參數放在rdi裏面,第二個參數放在rsi裏面,而被調函數直接去這兩個寄存器裏面把參數拿出來。這裏還有個細節,傳遞給sum的兩個參數都是用的edi和esi而不是rdi和rsi,原因在於C語言中int是32位的,而rdi和rsi都是64位的,edi和esi可以分別當成rdi和rsi的一部分來使用。

回到正題,執行完這4條指令後棧和寄存器的狀態圖(註意,下圖中的argc使用的是圖中連續8字節內存中的高4字節,低4字節未用):

技術分享圖片

參數準備好了之後,接著執行call指令調用sum函數,

0x0000000000400559<+25>:callq 0x400526 <sum>  #調用sum函數

call指令有點特殊,剛開始執行它的時候rip指向的是call指令的下一條指令,也就是說rip寄存器的值是0x40055e這個地址,但在call指令執行過程中,call指令會把當前rip的值(0x40055e)入棧,然後把rip的值修改為call指令後面的操作數,這裏是0x400526,也就是sum函數第一條指令的地址,這樣cpu就會跳轉到sum函數去執行。

call指令執行完成後棧及寄存器的狀態如下圖所示,可以看到rip已經指向了sum函數的第1條指令,sum函數執行完成返回之後需要執行的指令的地址0x40055e也已經保存到了main函數的棧幀之中。

技術分享圖片技術分享圖片

由於在main中執行了調用sum函數的call指令,CPU現在跳轉到sum函數開始執行,

0x0000000000400526<+0>:push   %rbp         
0x0000000000400527<+1>:mov   %rsp,%rbp
0x000000000040052a<+4>:mov   %edi,-0x14(%rbp)  
0x000000000040052d<+7>:mov   %esi,-0x18(%rbp)  
0x0000000000400530<+10>:mov   -0x14(%rbp),%edx
0x0000000000400533<+13>:mov   -0x18(%rbp),%eax
0x0000000000400536<+16>:add   %edx,%eax
0x0000000000400538<+18>:mov   %eax,-0x4(%rbp)
0x000000000040053b<+21>:mov   -0x4(%rbp),%eax
0x000000000040053e<+24>:pop   %rbp
0x000000000040053f<+25>:retq  

sum函數的前2條指令跟main函數前兩條指令一模一樣,

0x0000000000400526<+0>:push   %rbp           # sum函數序言,保存調用者的rbp
0x0000000000400527<+1>:mov   %rsp,%rbp  # sum函數序言,調整rbp寄存器指向自己的棧幀起始位置

都是在保存調用者的rbp然後設置新值使其指向當前函數棧幀的起始位置,這裏sum函數保存了main函數的rbp寄存器的值(0x7fffffffe510),並使rbp寄存器指向了自己的棧幀的起始位置(地址為0x7fffffffe4e0)。

可以看到,sum的函數序言並未像main函數一樣通過調整rsp寄存器的值來給sum函數預留用於局部變量和臨時變量的棧空間,那這是不是說明sum函數就沒有使用棧來保存局部變量呢,其實不是,從後面的分析可以看到,sum函數的局部變量s還是保存在棧上的。沒有預留為什麽可以使用呢,原因前面也說過,棧上的內存不需要在應用層代碼中分配,操作系統已經給我們分配好了,盡管用就行了。main函數之所以需要調整rsp寄存器的值是因為它需要使用call指令來調用sum函數,而call指令會自動把rsp寄存器的值減去8然後把函數的返回地址保存到rsp所指的棧內存位置,如果main函數不調整rsp的值,則call指令保存函數返回地址時會覆蓋局部變量或臨時變量的值;而sum函數中沒有任何指令會自動使用rsp寄存器來保存數據到棧上,所以不需要調整rsp寄存器。

緊接著的4條指令,

0x000000000040052a<+4>:mov   %edi,-0x14(%rbp)  # 把第1個參數a放入臨時變量
0x000000000040052d<+7>:mov   %esi,-0x18(%rbp)  # 把第2個參數b放入臨時變量
0x0000000000400530<+10>:mov   -0x14(%rbp),%edx# 從臨時變量中讀取第1個到edx寄存器
0x0000000000400533<+13>:mov   -0x18(%rbp),%eax# 從臨時變量中讀取第2個到eax寄存器

通過rbp寄存器加偏移量的方式把main傳遞給sum的參數保存在當前棧幀的合適位置,然後又取出來放入寄存器,這裏有點多此一舉,因為我們編譯的時候未給gcc指定優化級別,gcc編譯程序時默認不做任何優化,所以代碼看起來比較啰嗦。

緊接著的幾條指令

0x0000000000400536<+16>:add   %edx,%eax           # 執行a + b並把結果保存到eax寄存器
0x0000000000400538<+18>:mov   %eax,-0x4(%rbp)  # 把加法結果賦值給變量s
0x000000000040053b<+21>:mov   -0x4(%rbp),%eax # 讀取s變量的值到eax寄存器

第一條add指令負責執行加法運算並把結果3存入eax寄存器,第二條指令負責把eax寄存器的值保存到了s變量所在的內存,第三條指令又把s變量的值讀取到eax寄存器,可以看到局部變量s被編譯器安排在了rbp - 0x4這個地址所對應的內存之中。

到此,sum函數的主要功能已經完成,在繼續執行最後的兩條指令之前我們先來看看寄存器和棧的狀態:

技術分享圖片

上圖需要說明一點:

  • sum函數的兩個參數和返回值都是int型的,在內存中只占4個字節,而我們的示意圖中每個棧內存單元占8個字節且按8字節地址邊界進行了對齊,所以才是現在示意圖中的這個樣子。

我們來繼續執行pop %rbp這條指令,這條指令包含兩個操作:

  1. 把當前rsp所指棧內存中的值放入rbp寄存器,這樣rbp就恢復到了還未執行sum函數的第一條指令時的值,也就是重新指向了main函數的棧幀起始地址。

  2. 把rsp寄存器中的值加8,這樣rsp就指向了包含0x40055e這個值的棧內存,而這個棧單元中的值是當初main函數調用sum函數時call指令放入的,放入的這個值就是緊跟在call指令後面的下一條指令的地址。

還是來看看示意圖:

技術分享圖片

技術分享圖片

繼續retq指令,該指令把rsp指向的棧單元中的0x40055e取出給rip寄存器,同時rsp加8,這樣rip寄存器中的值就變成了main函數中調用sum的call指令的下一條指令,於是就返回到main函數中繼續執行。註意此時eax寄存器中的值是3,也就是sum函數執行後的返回值。還是來看一下狀態。

技術分享圖片

繼續執行main函數中的

mov  %eax,-0x4(%rbp)  # 把sum函數的返回值賦給變量n

該指令把eax寄存器中的值(3)放入rbp - 4所指的內存,這裏是變量n所在的位置,所以這條語句其實就是把sum函數的返回值賦值給變量n。這時狀態為:

技術分享圖片

後面的幾條指令

0x0000000000400561<+33>:mov   -0x4(%rbp),%eax
0x0000000000400564<+36>:mov   %eax,%esi
0x0000000000400566<+38>:mov   $0x400604,%edi
0x000000000040056b<+43>:mov   $0x0,%eax
0x0000000000400570<+48>:callq 0x400400 <[email protected]>
0x0000000000400575<+53>:mov   $0x0,%eax

首先為printf函數準備參數然後調用printf函數,在此我們就不分析它們了,因為調用printf和sum的過程差不多,我們讓CPU快速執行完這幾條指令然後暫停在main函數的倒數第二條的leaveq指令處,這時棧和寄存器狀態如下:

技術分享圖片

leaveq指令上面的一條指令mov $0x0, %eax的作用在於把main函數的返回值0放在eax寄存器中,等main返回後調用main函數的函數可以拿到這個返回值。下面執行leaveq指令,

0x000000000040057a<+58>:leaveq

該指令相當於如下兩條指令:

mov%rbp, %rsp
pop%rbp

leaveq指令首先把rbp寄存器中的值復制給rsp,這樣rsp就指向了rbp所指的棧單元,然後把使該內存單元中的值POP給rbp寄存器,這樣rbp和rsp的值就恢復成剛剛進入main函數時的狀態了。看圖:

技術分享圖片

到此main函數就只剩下retq指令了,前面分析sum函數時已經詳細分析過它了,這條指令執行完成後就會完全返回到調用main函數的函數中去繼續執行。

go語言中的函數調用過程

前面花了很大篇幅分析了C語言的函數調用過程,包括參數的傳遞,call指令,ret指令,還有返回值如何從被調用函數返回給調用函數的,有了這些基礎, 接下來我們來看go語言中的函數調用過程,其實二者原理是一樣的,只是細節上有一點差異。還是用一個簡單的例子來分析。

packagemain

//計算a, b的平方和
funcsum(a, bint) int{
    a2:=a*a
    b2:=b*b
    c:=a2+b2

    returnc
}

func main() {
   sum(1, 2)
}

使用go build編譯該程序,註意這裏需要指定 -gcflags "-N -l" 關閉編譯器優化,否則編譯器可能把對sum函數的調用優化掉。

[email protected]:~/study/go$ gobuild  -gcflags"-N -l"sum.go

編譯後得到二進制可執行程序sum, 首先來看main函數的反匯編代碼:

Dumpofassemblercodeforfunctionmain.main:
  0x000000000044f4e0<+0>: mov   %fs:0xfffffffffffffff8,%rcx #暫時不關註
  0x000000000044f4e9<+9>: cmp   0x10(%rcx),%rsp             #暫時不關註
  0x000000000044f4ed<+13>: jbe   0x44f51d <main.main+61>    #暫時不關註
  0x000000000044f4ef<+15>: sub   $0x20,%rsp                 #為main函數預留32字節棧空間
  0x000000000044f4f3<+19>: mov   %rbp,0x18(%rsp)            #保存調用者的rbp寄存器
  0x000000000044f4f8<+24>: lea   0x18(%rsp),%rbp            #調整rbp使其指向main函數棧幀開始地址
  0x000000000044f4fd<+29>: movq   $0x1,(%rsp)               #sum函數的第一個參數(1)入棧
  0x000000000044f505<+37>: movq   $0x2,0x8(%rsp)            #sum函數的第二個參數(2)入棧
  0x000000000044f50e<+46>: callq 0x44f480 <main.sum>        #調用sum函數
  0x000000000044f513<+51>: mov   0x18(%rsp),%rbp            #恢復rbp寄存器的值為調用者的rbp
  0x000000000044f518<+56>: add   $0x20,%rsp                 #調整rsp使其指向保存有調用者返回地址的棧單元
  0x000000000044f51c<+60>: retq                             #返回到調用者
  0x000000000044f51d<+61>: callq 0x447390 <runtime.morestack_noctxt> #暫時不關註
  0x000000000044f522<+66>: jmp   0x44f4e0 <main.main>                #暫時不關註
Endofassemblerdump.

main函數前面三條和最後兩條指令是go編譯器插入用於檢查棧溢出的代碼,我們現在不需要關註。其它部分跟C語言中的函數差不多,不過有點差別的是go語言函數調用時參數放在了棧上(第7和第8條指令把參數放在了棧上),從第4條指令可以看出,編譯器給main函數預留了32個字節用於存放main的棧基址rbp、調用sum函數時的兩個參數,這三項各占8個字節所以共占24字節,那還有8個字節拿來幹什麽的呢?從下面的sum函數可以看出來,剩下的8個字節用於存放sum函數的返回值。

 
Dumpofassemblercodeforfunctionmain.sum:
  0x000000000044f480<+0>: sub   $0x20,%rsp          #為sum函數預留32字節的棧空間
  0x000000000044f484<+4>: mov   %rbp,0x18(%rsp)     #保存main函數的rbp
  0x000000000044f489<+9>: lea   0x18(%rsp),%rbp     #設置sum函數的rbp
  0x000000000044f48e<+14>: movq   $0x0,0x38(%rsp)   #返回值初始化為0
  0x000000000044f497<+23>: mov   0x28(%rsp),%rax    #從內存中讀取第一個參數a(1)到rax
  0x000000000044f49c<+28>: mov   0x28(%rsp),%rcx    #從內存中讀取第一個參數a(1)到rcx
  0x000000000044f4a1<+33>: imul   %rax,%rcx         #計算a * a,並把結果放在rcx
  0x000000000044f4a5<+37>: mov   %rcx,0x10(%rsp)    #把rcx的值(a * a)賦值給變量a2
  0x000000000044f4aa<+42>: mov   0x30(%rsp),%rax    #從內存中讀取第二個參數a(2)到rax
  0x000000000044f4af<+47>: mov   0x30(%rsp),%rcx    #從內存中讀取第二個參數a(2)到rcx
  0x000000000044f4b4<+52>: imul   %rax,%rcx         #計算b * b,並把結果放在rcx
  0x000000000044f4b8<+56>: mov   %rcx,0x8(%rsp)     #把rcx的值(b * b)賦值給變量b2
  0x000000000044f4bd<+61>: mov   0x10(%rsp),%rax    #從內存中讀取a2到寄存器rax
  0x000000000044f4c2<+66>: add   %rcx,%rax          #計算a2 + b2,並把結果保存在rax
  0x000000000044f4c5<+69>: mov   %rax,(%rsp)        #把rax賦值給變量c, c = a2 + b2
  0x000000000044f4c9<+73>: mov   %rax,0x38(%rsp)    #將rax的值(a2 + b2)復制給返回值
  0x000000000044f4ce<+78>: mov   0x18(%rsp),%rbp    #恢復main函數的rbp
  0x000000000044f4d3<+83>: add   $0x20,%rsp         #調整rsp使其指向保存有返回地址的棧單元
  0x000000000044f4d7<+87>: retq                     #返回main函數
Endofassemblerdump.

 

sum函數的匯編代碼比較直觀,基本上就是對go語言sum函數的直接翻譯,可以看到sum函數通過rsp寄存器從main函數棧中獲取參數,返回值也通過rsp保存在了main函數的棧幀中。

下圖是已經執行完成sum函數的0x000000000044f4c9 <+73>: mov %rax,0x38(%rsp)這條指令但還未開始執行下一條指令時棧以及棧寄存器之間的關系圖,讀者可以結合上面的匯編代碼和該圖,加深對函數調用過程中的參數傳遞、返回值以及局部變量在棧上的位置和關系的理解。

技術分享圖片

總結

最後我們來總結一下函數調用過程:

  • 參數傳遞。gcc編譯的c/c++代碼一般通過寄存器傳遞參數,在AMD64 Linux 平臺,gcc約定函數調用時前面6個參數分別通過rdi, rsi, rdx, r10, r8及r9傳遞;而go語言函數調用時參數是通過棧傳遞給被調用函數的,最後一個參數最先入棧,第一個參數最後入棧,參數在調用者的棧幀之中,被調用函數通過rsp加一定的偏移量來獲取參數;
  • call指令負責把執行call指令時的rip寄存器(函數返回地址)入棧;
  • gcc通過rbp加偏移量的方式來訪問局部和臨時變量,而go編譯器則使用rsp寄存器加偏移量的方式來訪問它們;
  • ret指令負責把call指令入棧的返回地址出棧給rip,從而實現從被調用函數返回到調用函數繼續執行;
  • gcc使用rax寄存器返回函數調用的返回值,而go使用棧返回函數調用的返回值。

從匯編層面看函數調用的實現原理