從匯編語言的寄存器來看函數參數傳遞


本篇的介紹順序是:

  1. 代碼在內存中的分布
  2. 匯編語言翻譯的代碼
  3. 用匯編語言來看函數傳參

代碼在內存中的分布

代碼在執行時就是系統當中的一個進程,每一個系統進程擁有一個4G空間的虛擬內存。代碼在執行時從硬盤上被加載到內存中,那么在這個4G空間的內存中是如何分布的呢?請看下面的分布

進程地址空間中最頂部的段是棧,
作用:大多數編程語言將之用於存儲函數參數和局部變量。
工作過程:調用一個方法或函數會將一個新的棧幀(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語言執行的過程就有:預處理--->編譯--->匯編--->鏈接

匯編語言
硬件真正執行的是機器語言,類似於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. 傳地址調用是將數值的地址拷貝一份到被調用函數中,數值在內存中只有一份,被調用函數通過該地址還能找到數值,可以修改這個數值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM