從暫存器來看函數參數傳遞

程式碼在記憶體中的分布

程式碼在執行時就是系統當中的一個進程,每一個系統進程擁有一個4G空間的虛擬記憶體。程式碼在執行時從硬碟上被載入到記憶體中,那麼在這個4G空間的記憶體中是如何分布的呢?請看下面的分布

進程地址空間中最頂部的段是棧,
作用:大多數程式語言將之用於存儲函數參數和局部變數。
工作過程:調用一個方法或函數會將一個新的棧幀(stack frame)壓入到棧中,這個棧幀會在函數返回時被清理掉。
優點:由於棧中數據嚴格的遵守FIFO的順序,這個簡單的設計意味著不必使用複雜的數據結構來追蹤棧中的內容,只需要一個簡單的指針指向棧的頂端即可,因此壓棧(pushing)和退棧(popping)過程非常迅速、準確。進程中的每一個執行緒都有屬於自己的棧。

與棧一樣,堆用於運行時記憶體分配;但不同的是,堆用於存儲那些生存期與函數調用無關的數據。
作用:堆用於存儲那些生存期與函數調用無關的數據。
優點:大部分語言都提供了堆管理功能。在C語言中,堆分配的介面是malloc()函數。如果堆中有足夠的空間來滿足記憶體請求,它就可以被語言運行時庫處理而不需要內核參與,否則,堆會被擴大,通過brk()系統調用來分配請求所需的記憶體塊。

.bss

BSS保存的是未被初始化的靜態變數內容,如果你寫static intcntActiveUsers ,則cntActiveUsers的內容就會保存到BSS中去。

.data

數據段保存在源程式碼中已經初始化的靜態變數的內容。也就是源程式碼中指定了初始值的靜態變數。如果你寫static int cntActiveUsers=10,則cntActiveUsers的內容就保存在了數據段中,而且初始值是10。

.text

程式碼段,主要保存程式的程式碼以及編譯時靜態鏈接進來的庫。這段記憶體大小在程式運行之前就已經確定,而且是只讀,可能存在一些常量,比如字元串常量。

程式碼在運行時,以上欄位如何在記憶體中分布,可以參考這篇文章,讓記憶體看得見摸得著。
//blog.csdn.net/ljianhui/article/details/21666327

認識彙編

程式語言從面向對象的不同可以分為低級語言和高級語言。低級語言面向機器編程,如機器語言,彙編語言;高級語言面向過程和對象編程,如C、Java、Python、Go等。
低級語言更加接近電腦硬體,所以也能更加清晰的看出一個程式在執行時指令讓硬體做什麼了。並且高級語言往往都是編譯成低級語言,再交給硬體執行。如典型的C語言執行的過程就有:預處理—>編譯—>彙編—>鏈接

彙編語言
硬體真正執行的是機器語言,類似於010101001的二進位,特點是最接近機器硬體,執行速度快,但是編寫程式比較複雜。而彙編語言是為了解決編寫機器語言複雜度。
彙編語言用一些容易理解和記憶的字母,單詞來代替一個特定的指令,比如:用ADD代表數字邏輯上的加減,MOV代表數據傳遞等等,通過這種方法,人們很容易去閱讀已經完成的程式或者理解程式正在執行的功能,對現有程式的bug修復以及運營維護都變得更加簡單方便。

彙編demo

以C語言為例子,寫一個最簡單的C語言程式,編譯出彙編語言。

#include<stdio.h>

int main()
{
	int a = 10;
    
	return 0;
}
gcc -S hello.c -o hello.s

彙編文件中,以.開頭是偽指令。偽指令是是輔助性的,彙編器在生成目標文件時會用到這些資訊,但偽指令不是真正的 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

彙編指令

%:表示一個暫存器。

暫存器名前有%前綴。例如,如果要使用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<stdio.h>

void fun(int x)
{
    int y;
    y = x + 20;
}


int main()
{
	int a = 10;
        fun(a);    
	return 0;
}

gcc -S hello.c -o hello

傳地址調用

#include <stdio.h>

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

函數調用傳參總結

傳值調用和傳地址調用最大區別就在於調用函數處理實參的方式,傳值調用,就是將數值當做實參寫入暫存器,被調用函數從暫存器中取出數值;傳地址調用是將數值的地址當作實參寫入暫存器,被調用函數中從暫存器取出地址。

傳值調用

傳地址調用

無論是傳值還是傳地址,都是將調用函數中的實參拷貝一份傳遞給被調用函數的形參。只不過區別在於:

  1. 傳值調用直接拷貝一份數值到被調用函數,被調用函數中的數值和調用函數中的數值在記憶體中是兩份相互獨立的;
  2. 傳地址調用是將數值的地址拷貝一份到被調用函數中,數值在記憶體中只有一份,被調用函數通過該地址還能找到數值,可以修改這個數值。