【彙編語言與計算機系統結構筆記09】程序棧,(x86-32)過程調用,棧幀,寄存器使用慣例

本次筆記內容:
10.棧與過程調用的機器表示-1
11.棧與過程調用的機器表示-2
12.實驗

前言

首先複習了上節課內容。

消除部分數據相關是有必要的,爲了提高效率,可以使用 Partial Register Stall 等技術。

條件跳轉指令可能對流水線效率造成傷害。

x86-32的程序棧

  • 符合“棧(stack)”工作原理的一塊內存區域,從高地址向低地址“增長”。
  • %esp存儲棧頂位置(儘量使esp指向當前棧的棧頂)。
棧底
↑ Increasing Addresses
↓ Stack Grows Down
棧頂指針 %esp 棧頂

壓棧操作

pushl Src

  • 從Src取得操作數
  • %esp = %esp - 4
  • 寫入棧頂地址
棧底
↑ Increasing Addresses
↓ Stack Grows Down
%esp 本來指向這裏,壓棧後-4,指向下面
棧頂指針 %esp 棧頂

出棧操作

popl Dest

  • 讀取棧頂數據(%esp)
  • %esp = %esp + 4
  • 寫入Dest
棧底
↑ Increasing Addresses
↓ Stack Grows Down
棧頂指針 %esp 棧頂
%esp 本來指向這裏,出棧後+4,指向上面

過程調用

  • 利用棧支持過程調用與返回

過程調用指令:call label,將返回地址壓入棧,跳轉至label。

返回地址:call指令的下一條地址。彙編實例如下。

804854e: e8 3d 06 00 00 call 8048b90 <main>
8048553: 50             pushl %eax

Return address = 0x8048553

過程返回指令:ret,跳轉至棧頂的返回地址。

我理解,其作用爲,執行 call 後面的函數,執行結束後,在回到本線程來。call即,我在執行前,先把當前線程執行到哪裏了,做個標記,壓棧。

基於棧的編程語言

支持遞歸:

  • e.g. C, Pascal, Java
  • 代碼時可重入的(Reentrant),同時有同一個過程的多個實例在運行;
  • 因此需要有一塊區域來存儲每個過程實例的數據,包括參數、局部變量、返回地址。

棧的工作規律:

  • 每個過程實例的運行時間是有限的,即棧的有效時間有限:From when called to when return;
  • 被調用者先於調用者返回(一般情況下,如果遇到異常處理情況,則不是這個樣子)。

每個過程實例在棧中維護一個棧幀(stack frame)。

棧幀

在這裏插入圖片描述

棧幀(stack frame)存儲內容:

  • 局部變量;
  • 返回地址;
  • 臨時空間

棧幀的分配與釋放:

  • 進入過程後先“分配”棧幀空間,“Set-up” code;
  • 過程返回時“釋放”,“Finish” code。
  • 寄存器%esp指向當前棧幀的起始地址。

在這裏插入圖片描述

過程調用時棧的變化:

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

x86-32/Linux下的棧幀

當前棧真的內容(自“頂”向下)

  • 子過程參數:“Argument build”;
  • 局部變量,因爲通用寄存器個數有限;
  • 被保存的寄存器值;
  • 父過程的棧幀起始地址(old %ebp)

父過程的棧幀中與當前過程相關的內容:

  • 返回地址,由call指令存入
  • 當前過程的輸入參數;
  • etc.
Caller Frame
Caller Frame Arguments
Caller Frame Return Addr
棧幀指針(%esp) Old %ebp
Saved Registers + Local Variables
棧頂指針(%esp) Argument Build

以swap過程爲例

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

如上圖,當前%ebp還是父過程%ebp,因此Setup現將其存儲,留着以後恢復。

在這裏插入圖片描述

之後將當前(新的)%ebp指向舊的%ebp,即設好之後工作的基址。

在這裏插入圖片描述

之後push %ebx,因爲儘管父過程可能用%ebx,爲了安全,要保存一下。

當然,也不是所有的實例的寄存器都要存。以後講。

在這裏插入圖片描述

抽象的堆棧和實際的棧的對應關係如上圖。

在這裏插入圖片描述

Finish在調用結束後,將父過程恢復。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

寄存器使用慣例

爲什麼設置“使用慣例”

過程yoo調用who:

  • yoo:caller
  • who:callee

做一個軟件層面的約定:哪些寄存器由調用者保存,哪些由被調用者保存。

如何使用寄存器作爲程序的臨時存儲?

yoo:
	...
	movl $15213, %edx
	call who
	addl %edx, %eax
	...
	ret

who:
	...
	movl 8(%ebp), %edx
	addl $91125, %edx
	...
	ret

如上例,%edx可能被yoo和who同時重複保存恢復,因此作出約定:

使用慣例:

  • 調用者負責保存:caller在調用子過程之前將這些寄存器內容存儲在它的棧幀內;
  • 被調用者負責保存:callee在使用這些寄存器之前將其原有內容存儲在它的棧幀內。

x86-32/Linux下的使用慣例

8個Registers:

  • 兩個特殊寄存器%ebp,%esp
  • 三個由調用者負責保存:%ebx,%esi,%edi
  • 三個由被調用者負責保存:%eax,%edx,%ecx
  • %eax用於保存過程返回值

遞歸調用例子

int rfact(int x) {
	int rval;
	if (x <= 1)
		return 1;
	rval = rfact(x - 1);
	return reval * x;
}

寄存器使用情況:

  • %eax直接使用;
  • %ebx使用前保存舊值,退出前恢復。
.globl rfact
	.type
rfact, @function
rfact:
	pushl %ebp
	movl %esp, %ebp
	pushl %ebx			# Set up
	movl 8(%ebp), %ebx
	cmpl $1, %ebx
	jle .L78
	leal -1(%ebx), %eax
	pushl %eax
	call rfact
	imull %ebx
	jmp .L79
	.align 4
.L78:
	movl $1, %eax
.L79:
	movl -4(%ebp), %ebx
	movl %ebp, %esp
	popl %ebp
	ret

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

帶指針的“階乘”過程

// Recursive Procedure
void s_helper(int x, int *accum) {
	if (x <= 1)
		return;
	else {
		int z = *accum * x;
		*accum = z;
		s_helper(x - 1, accum);
	}
}
// Top-Level Call
int sfact(int x) {
	int val = 1;
	s_helper(x, &val);
	return val;
}

首先,創建指針,如下圖。

在這裏插入圖片描述

在這裏插入圖片描述

如上圖,可以認識到,在編程中不能把臨時變量的地址return。

之所以將%esp增加16 bytes,是因爲很多機器(x86-32)中要求棧16 bytes對齊。

在這裏插入圖片描述

接下來,傳遞指針。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

因此,如上圖,在使用指針時,就如上圖:

  • %ecx存儲變量x;
  • %edx存儲變量accum。

x86-32過程調用小結

程序棧:

  • 各個過程運行實例的私有空間:不同實例間避免相互干擾,過程本地變量與參數存於棧內(採用相對於棧基址%ebp的尋址)
  • 符合棧的基本工作規律:過程返回順序與過程調用的順序相反

相關指令與寄存器使用慣例:

  • Call / Ret指令
  • 寄存器使用慣例:調用者/被調用者保存,%ebp/%esp兩個特殊奇存器
  • 棧幀的存儲內容

x86-64通用寄存器與過程調用

寄存器 慣例 寄存器 慣例
%rax Return Value %r8 Argument #5
%rbx Callee Saved %r9 Argument #6
%rcx Argument #4 %r10 Callee Saved
%rdx Argument #3 %r11 Used for linking
%rsi Argument #2 %r12 C: Callee Saved
%rdi Argument #1 %r13 Callee Saved
%rsp Stack Pointer %r14 Callee Saved
%rbp Callee Saved %r15 Callee Saved

x86-64寄存器

過程參數(不超過6個)通過寄存器傳遞:

  • 大於6個的仍使用棧傳遞;
  • 這些傳遞參數的寄存器可以看成是“調用者保存”寄存器。

所有對於棧幀內容的訪問都是基於%esp完成的:

  • %ebp完全用作通用寄存器。

例:x86-64下的swap過程 - 1

void swap(long *xp, long *yp) {
	long t0 = *xp;
	long t1 = *yp;
	*xp = t1;
	*yp = t0;
}
swap:
	movq (%rdi), %rdx
	movq (%rsi), %rax
	movq %rax, (%rdi)
	movq %rdx, (%rsi)
	ret

參數由寄存器傳遞:

  • First (xp) in %rdi, second (yp) in %rsi
  • 64位指針

無需任何棧操作:

  • 局部變量也存儲於寄存器中。

例:x86-64下的swap過程 - 2

/* Swap, using local array */
void swap_a(long *xp, long *yp) {
	volatile long loc[2];
	loc[0] = *xp;
	loc[1] = *yp;
	*xp = loc[1];
	*yp = loc[0];
}

其中,使用 volatile關鍵字 強制使用棧空間,但在實際使用中沒有修改棧頂寄存器(%rsp)。

swap_a:
	movq (%rdi), %rax
	movq %rax, -24(%rsp)
	movq (%rsi), %rax
	movq %rax, -16(%rsp)
	movq -16(%rsp), %rax
	movq %rax, (%rdi)
	movq -24(%rsp), %rax
	movq %rax, (%rsi)
	ret

在這裏插入圖片描述

例:x86-64下的swap過程 - 3

long scount = 0;
/* Swap a[i] & a[i+1] */
void swap_ele_se(long a[], int i) {
	swap(&a[i], &a[i+1]);
	scount++;
}
swap_ele_se:
	movslq %esi, %rsi			# Sign extend i
	leaq (%rdi, %rsi, 8), %rdi	# &a[i]
	leaq 8(%rdi), %rsi			# &a[i+1]
	call swap					# swap()
	incq scount(%rip)			# scount++;
	ret

incq scont(%rip) 是把變量加1。

在x86下引入新尋址方式:

  • 相對於當前指令(%rip)的尋址;
  • 因爲程序可能有動態鏈接庫dll,而在dll中我們無法確定絕對位置,但是知道相對位置。

爲什麼swap_ele_se沒有分配棧幀?

因爲(除返回值外)沒有私有數據來保留,用不着。

例:x86-64下的swap過程 - 4

long scount = 0;
/* Swap a[i] & a[i+1] */
void swap_ele(long a[], int i) {
	swap(&a[i], &a[i+1]);
}
swap_ele:
	movslq %esi, %rsi			# Sign extend i
	leaq (%rdi, %rsi, 8), %rdi	# &a[i]
	leaq 8(%rdi), %rsi			# &a[i+1]
	jmp swap					# swap

使用jmp指令調用過程,可以是因爲對棧沒有什麼變化。

x86-64的棧幀使用實例

long sum = 0;
/* Swap a[i] & a[i+1] */
void swap_ele_su(long a[], int i) {
	swap(&a[i], &a[i+1];
	sum += a[i];
}
swap_ele_su:
	movq %rbx, -16(%rsp)
	movslq %esi, %rbx
	movq %r12, -8(%rsp)
	movq %rdi, %r12
	leaq (%rdi, %rbx, 8), %rdi
	subq $16, %rsp
	leaq 8(%rdi), %rsi
	call swap
	movq (%r12, %rbx, 8), %rax
	addq %rax, sum(%rip)
	movq (%rsp), %rbx
	movq 8(%rsp), %r12
	addq $16, %rsp
	ret
  • 變量a與i的值存於“被調用者保存”的寄存器中;
  • 因此必須分配棧幀來保存這些寄存器。

實驗作業

在這裏插入圖片描述

兩個,BombLab與BufLab。

發佈了139 篇原創文章 · 獲贊 48 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章