1. 程式人生 > >從暫存器來看函式引數傳遞

從暫存器來看函式引數傳遞

![](https://images.cnblogs.com/cnblogs_com/goldsunshine/1689496/o_200405143454three.jpg) # 程式碼在記憶體中的分佈 程式碼在執行時就是系統當中的一個程序,每一個系統程序擁有一個4G空間的虛擬記憶體。程式碼在執行時從硬碟上被載入到記憶體中,那麼在這個4G空間的記憶體中是如何分佈的呢?請看下面的分佈 ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320110718042-1542235323.png) ## 棧 程序地址空間中最頂部的段是棧, `作用`:大多數程式語言將之用於儲存函式引數和區域性變數。 `工作過程`:呼叫一個方法或函式會將一個新的棧幀(stack frame)壓入到棧中,這個棧幀會在函式返回時被清理掉。 `優點`:由於棧中資料嚴格的遵守FIFO的順序,這個簡單的設計意味著不必使用複雜的資料結構來追蹤棧中的內容,只需要一個簡單的指標指向棧的頂端即可,因此壓棧(pushing)和退棧(popping)過程非常迅速、準確。程序中的每一個執行緒都有屬於自己的棧。 ## 堆 與棧一樣,堆用於執行時記憶體分配;但不同的是,堆用於儲存那些生存期與函式呼叫無關的資料。 `作用`:堆用於儲存那些生存期與函式呼叫無關的資料。 `優點`:大部分語言都提供了堆管理功能。在C語言中,堆分配的介面是malloc()函式。如果堆中有足夠的空間來滿足記憶體請求,它就可以被語言執行時庫處理而不需要核心參與,否則,堆會被擴大,通過brk()系統呼叫來分配請求所需的記憶體塊。 ## .bss BSS儲存的是未被初始化的靜態變數內容,如果你寫`static intcntActiveUsers `,則`cntActiveUsers`的內容就會儲存到BSS中去。 ## .data 資料段儲存在原始碼中已經初始化的靜態變數的內容。也就是原始碼中指定了初始值的靜態變數。如果你寫`static int cntActiveUsers=10`,則`cntActiveUsers`的內容就儲存在了資料段中,而且初始值是10。 ## .text 程式碼段,主要儲存程式的程式碼以及編譯時靜態連結進來的庫。這段記憶體大小在程式執行之前就已經確定,而且是隻讀,可能存在一些常量,比如字串常量。 程式碼在執行時,以上欄位如何在記憶體中分佈,可以參考這篇文章,讓記憶體看得見摸得著。 https://blog.csdn.net/ljianhui/article/details/21666327 # 認識彙編 程式語言從面向物件的不同可以分為低階語言和高階語言。低階語言面向機器程式設計,如機器語言,組合語言;高階語言面向過程和物件程式設計,如C、Java、Python、Go等。 低階語言更加接近計算機硬體,所以也能更加清晰的看出一個程式在執行時指令讓硬體做什麼了。並且高階語言往往都是編譯成低階語言,再交給硬體執行。如典型的C語言執行的過程就有:`預處理`--->`編譯`--->`彙編`--->`連結`。 ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320115658079-2007330539.png) `組合語言` 硬體真正執行的是機器語言,類似於`010101001`的二進位制,特點是最接近機器硬體,執行速度快,但是編寫程式比較複雜。而組合語言是為了解決編寫機器語言複雜度。 組合語言用一些容易理解和記憶的字母,單詞來代替一個特定的指令,比如:用`ADD`代表數字邏輯上的加減,`MOV`代表資料傳遞等等,通過這種方法,人們很容易去閱讀已經完成的程式或者理解程式正在執行的功能,對現有程式的bug修復以及運營維護都變得更加簡單方便。 ## 彙編demo 以C語言為例子,寫一個最簡單的C語言程式,編譯出組合語言。 ```c #include int main() { int a = 10; return 0; } ``` ``` gcc -S hello.c -o hello.s ``` ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320124042831-294972450.png) ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320124159536-359818226.png) 彙編檔案中,以.開頭是偽指令。偽指令是是輔助性的,彙編器在生成目標檔案時會用到這些資訊,但偽指令不是真正的 CPU 指令,而是寫給彙編器的。每種彙編器的偽指令也不同,要查閱相應的手冊。 `.file`指明檔名字 `.text`指明記憶體中的程式碼段 `.globl`指明全域性變數 其他內容無關緊要,下面把偽指令去掉再分析彙編檔案。 ``` main: .LFB0: pushq %rbp #rbp儲存是main函式的地址,將其入棧,為函式的棧底 movq %rsp, %rbp #rsp代表main函式的棧頂,此時開闢棧頂和棧底 movl $10, -4(%rbp) #rbp暫存器儲存的是一個地址,將地址數值-4,然後將10放在該地址指向的記憶體空間中 movl $0, %eax #return 0,設定返回值0 popq %rbp #將棧底變數從棧裡彈出去,表示執行函式結束 ret #返回 .LFE0: .size main, .-main .ident "GCC: (Uos 8.3.0.3-3+rebuild) 8.3.0" .section .note.GNU-stack,"",@progbits ``` ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320131553031-654224665.png) ## 彙編指令 ### %:表示一個暫存器。 暫存器名前有%字首。例如,如果要使用eax,得寫作: %eax。 ### $:立即數表示法,表示一個數值。 $10就是表示數字10。所謂立即數:還沒放入記憶體之前的數就叫立即數,放入之後就不是了。立即數就是突然蹦出來的數,不是存到某些 容器(記憶體,暫存器)中的數 ### (%ebp):定址 表示以%ebp裡儲存的值為地址,找到該地址指向的記憶體裡的儲存的值。這個叫暫存器定址。`-4(%ebp)`表示 ebp-4,然後以這個值為地址,找到記憶體中該地址儲存的值 ### movl:移動指令 `movl $1234 %eax`,表示將數值1234移動到eax暫存器中。 ### sunq:減指令 `subq $16 %rsp`,sub將兩個運算元相減,用第二個運算元減去第一個運算元,將結果儲存的到第二個運算元。該指令是將棧頂指標rsp向下移動16個地址。 ### addq:加指令 `addq %rbx, %rax`表示rbx的值加上rax的值,寫到rax內。 ### lea:load effective address 載入有效地址 取地址傳送到指定的的暫存器。`leaq %123 %rax` 將數值123的地址移動到暫存器rax。類似於C語言中的”&”。 ### call:函式呼叫 `call fun`:呼叫函式fun,執行到這一個指令之後,就進入fun函式。在棧中新開闢一個函式的棧幀,進入fun的棧幀執行。 # 函式呼叫 ## 棧幀 函式呼叫包括將資料和控制從程式碼的一部分傳遞到另一部分。另外被呼叫函式有自己的區域性變數空間,在被呼叫函式退出時釋放這些空間。而大多數程式語言的資料傳遞、區域性變數的分配和釋放通過操縱程式棧來實現。為函式呼叫分配的那部分棧稱為`棧幀`。 `棧幀(stack frame)`:棧幀的主要作用是用來控制和儲存一個函式呼叫的所有資訊。機器用棧來傳遞過程引數,儲存返回資訊,儲存暫存器用於以後恢復以及本地儲存。棧幀其實是兩個指標暫存器,暫存器`%ebp`為棧底指標,指向該棧幀的最底部,而暫存器`%esp`為棧頂指標,指向該棧幀的最頂部。當程式執行時,棧指標可以移動,並且大多數的資訊的訪問都是通過棧底指標配合偏移量來完成。%ebp棧底指標是不移動的,訪問棧裡面的元素可以用-4(%ebp)或者8(%ebp)訪問%ebp指標下面或者上面的元素。 ## 函式呼叫過程 函式在呼叫過程中記憶體的變化: 1、在呼叫函式棧幀中將形參壓入當前棧 2、跳轉到被調函式 3、被調函式開闢新的棧幀 4、從暫存器獲取形參 5、執行指令後退出 ## 傳值呼叫 ``` #include void fun(int x) { int y; y = x + 20; } int main() { int a = 10; fun(a); return 0; } ``` ``` gcc -S hello.c -o hello ``` ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320134643043-1170002897.png) ## 傳地址呼叫 ``` #include void fun(int *x) { int y = 200; *x = y + *x; } int main() { int a = 10; fun(&a); return 0; } ``` ``` gcc -S hello.c -o hello ``` ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320154302799-60488610.png) ## 函式呼叫傳參總結 傳值呼叫和傳地址呼叫最大區別就在於呼叫函式處理實參的方式,傳值呼叫,就是將`數值`當做實參寫入暫存器,被呼叫函式從暫存器中取出數值;傳地址呼叫是將`數值的地址`當作實參寫入暫存器,被呼叫函式中從暫存器取出地址。 `傳值呼叫` ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320160646888-1987792830.png) `傳地址呼叫` ![](https://img2020.cnblogs.com/blog/1060878/202103/1060878-20210320160554422-709665000.png) 無論是傳值還是傳地址,都是將呼叫函式中的實參拷貝一份傳遞給被呼叫函式的形參。只不過區別在於: 1. `傳值呼叫`直接拷貝一份`數值`到被呼叫函式,被呼叫函式中的數值和呼叫函式中的數值在記憶體中是兩份相互獨立的; 2. `傳地址呼叫`是將`數值的地址`拷貝一份到被呼叫函式中,數值在記憶體中只有一份,被呼叫函式通過該地址還能找到數值,可以修改這個