本次筆記內容:
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。