棧幀的實現實際跟系統本身的架構密切相關,這裏主要對i386和arm32兩種架構下的棧幀展開分析。
下面關於棧幀的分析都將基於這個例子進行:
int func(int a,int b)
{
int sum;
sum = a + b;
return sum;
}
int main(void)
{
int sum;
sum = func(1,2);
return 0;
}
1. i386中的棧幀
下面是i386架構系統上跟棧幀相關的一組寄存器:
4個數據寄存器(eax、ebx、ecx、edx) 這4個是通用數據寄存器,主要用於存放變量值,其中eax還用於傳遞函數返回值
ebp 基址指針寄存器,用於存放一個指針,指向當前棧幀的棧底
esp 棧指針寄存器,用於存放一個指針,指向當前棧幀的棧頂
eip 指令指針寄存器,用於存放一個指針,指向下一條將要執行的指令,該寄存器不能直接操作,只能通過call、ret指令間接操作
i386架構系統上當前函數對應的典型棧幀佈局:
^ ^ | |
| | | 函數實參n |
| 偏移爲正 | ...... |
| | +8 | 函數實參1 |
+4 | 返回地址,實際就是指向當前函數返回後下一條將要執行的指令 |
地址增大 ebp --> | 保存的前一個棧幀的ebp |
-4 | 函數局部變量1 |
| | | ...... |
| 偏移爲負 | ...... |
| | esp --> | 函數局部變量n |
| v | |
當函數被調用時,新的棧幀被壓入棧中,當函數返回時,相應的棧幀從棧中彈出。
ebp中存放的就是棧基址,指向當前函數的棧底,顯然在當前函數生命週期中該位置是固定的。
訪問當前函數的實參和局部變量的方法就是以ebp爲基址,再加上一個偏移,由上圖可知,實參的偏移爲正,局部變量的偏移爲負。
esp中存放的就是棧指針,指向當前函數的棧頂,顯然該位置在當前函數生命週期中會發生變化。
上面範例在i386中的彙編實現如下:
func:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %edx
movl 12(%ebp), %eax
addl %edx, %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
pushl $2
pushl $1
call func
addl $8, %esp
movl %eax, -4(%ebp)
movl $0, %eax
leave
ret
基於上面的彙編實現可知i386中一個函數棧幀的建立和銷燬過程:
建立過程:
[1]. 首先將新函數的實參從右向左依次壓棧
[2]. 接着調用call指令將eip中的地址壓棧作爲新函數棧幀銷燬後的返回地址,並將新函數的入口地址賦給eip,從而跳轉到新函數入口
[3]. 最後將ebp壓棧,並將esp賦給ebp,這一步確立了新函數的棧底和棧頂位置,也意味着新函數的棧幀框架正式建立
銷燬過程:
[1]. 如果函數有返回值,首先將返回值賦給eax
[2]. 接着調用leave指令將ebp賦給esp,並從新的棧頂彈出一個值賦給ebp,顯然這就是棧幀建立過程中步驟[3]的反過程
[3]. 最後調用ret指令從棧頂彈出一個值賦給eip,顯然這就是棧幀建立過程中步驟[2]的反過程。
至此,一個函數棧幀的銷燬過程實際上已經完成
2. arm32中的棧幀
相對於i386架構系統上相對統一的棧幀結構,arm32架構cpu上的棧幀實現就顯得非常多變,這裏將分析的是較典型的一種。
下面是arm32架構系統上跟棧幀相關的一組寄存器:
r0~r3 這些寄存器既用於傳遞函數的入參,又用於傳遞函數的返回值(通常只是r0),在當前函數運行期間還可用於存放普通變量
fp 棧幀指針寄存器,相當於i386中的ebp
sp 棧指針寄存器,相當於i386中的esp
pc 程序計數器,存放了下一條將要執行指令的位置,相當於i386中的eip
lr 連接寄存器,用於存放當前函數的返回地址,i386中返回地址被存放在棧中
上面範例在arm32中的彙編實現如下:
func:
str fp, [sp, #-4]!
add fp, sp, #0
sub sp, sp, #20
str r0, [fp, #-16]
str r1, [fp, #-20]
ldr r2, [fp, #-16]
ldr r3, [fp, #-20]
add r3, r2, r3
str r3, [fp, #-8]
ldr r3, [fp, #-8]
mov r0, r3
sub sp, fp, #0
ldr fp, [sp], #4
bx lr
main:
stmfd sp!, {fp, lr}
add fp, sp, #4
sub sp, sp, #8
mov r1, #2
mov r0, #1
bl func
str r0, [fp, #-8]
mov r3, #0
mov r0, r3
sub sp, fp, #4
ldmfd sp!, {fp, lr}
bx lr
基於上面的彙編實現可知arm32中一個函數棧幀的建立和銷燬過程:
建立過程:
[1]. 首先將新函數的實參從左向右依次賦給r0~r3,如果函數存在超過4個的情況,則多餘的參入從右向左依次壓棧
[2]. 接着調用bl指令將pc中的地址賦給lr作爲新函數棧幀銷燬後的返回地址,並將新函數的入口地址賦給pc,從而跳轉到新函數入口
[3]. 最後將fp壓棧,並將sp賦給fp,這一步確立了新函數的棧底和棧頂位置,也意味着新函數的棧幀框架正式建立
銷燬過程:
[1]. 如果函數有返回值,首先將返回值賦給r0
[2]. 接着將fp賦給sp,並從新的棧頂彈出一個值賦給fp,顯然這就是棧幀建立過程中步驟[3]的反過程
[3]. 最後調用bx指令,跳轉到lr指向的返回地址,顯然這就是棧幀建立過程中步驟[2]的反過程
至此,一個函數棧幀的銷燬過程實際上已經完成