重新補了一定的C基礎和C++基礎後,來結合csapp把函數棧幀簡單複習一下
1、什麼是函數棧幀
如果接觸過指針,知道指針的大小在32位下是四個位元組,在64位下是八個位元組。那麼指針的大小,就定義為一幀的大小。這就是棧幀。
什麼是函數棧幀呢?
首先在代碼的資料和函數存儲,可以大緻分為幾個區。
- 棧區:存放局部變量,形式參數
- 堆區:動态記憶體配置設定,如malloc,calloc,realloc,free
- 靜态區:存放全局變量,靜态變量
當然在連結那一塊更加細分,不過這裡大緻分成這樣就可以。
那函數棧幀實際上就是每一次的函數調用都要在記憶體中開辟空間,開辟的那一塊就叫函數棧幀。
那函數棧幀裡面存什麼,存放一些變量資料。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI2EzX4xSZz91ZsAzNfRHLGZkRGZkRfJ3bs92YsMTMfVmepNHL1skc1cWcZVDapJmNj1SS2A1QiVTQClGVF5UMR9Fd4VGdsATNfd3bkFGazxycykFaKdkYzZUbapXNXlleSdVY2pESa9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL2IzY4UWOzcjMwEmYkdTZ4MmYxQTNyYzM2YmNxADOyUzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
2、代碼的反彙編
我們可以在GCC反彙編或者VS反彙編下來觀察。兩者的彙編代碼文法略有不同。不過可以互通。這裡分别展示VS和GCC底下的反彙編。GCC反彙編想顯示代碼名字編譯的時候±g
#include<stdio.h>
using namespace std;
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
return 0;
}
這一串代碼背後到底發生了什麼?如何看到背後的問題,VS有反彙編。
注:反彙編要在調試開始後,在調試–視窗部分–反彙編。
- VS
int main()
{
00641770 push ebp
00641771 mov ebp,esp
00641773 sub esp,0E4h
00641779 push ebx
0064177A push esi
0064177B push edi
0064177C lea edi,[ebp-0E4h]
00641782 mov ecx,39h
00641787 mov eax,0CCCCCCCCh
0064178C rep stos dword ptr es:[edi]
0064178E mov ecx,offset [email protected] (064C003h)
00641793 call @[email protected] (064120Dh)
int a = 10;
00641798 mov dword ptr [a],0Ah
int b = 20;
0064179F mov dword ptr [b],14h
int c = 0;
006417A6 mov dword ptr [c],0
c = Add(a, b);
006417AD mov eax,dword ptr [b]
006417B0 push eax
006417B1 mov ecx,dword ptr [a]
006417B4 push ecx
006417B5 call Add (0641186h)
006417BA add esp,8
006417BD mov dword ptr [c],eax
return 0;
006417C0 xor eax,eax
}
- GCC
-
00000000000005fa <Add>: 5fa: 55 push %rbp 5fb: 48 89 e5 mov %rsp,%rbp 5fe: 89 7d ec mov %edi,-0x14(%rbp) 601: 89 75 e8 mov %esi,-0x18(%rbp) 604: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 60b: 8b 55 ec mov -0x14(%rbp),%edx 60e: 8b 45 e8 mov -0x18(%rbp),%eax 611: 01 d0 add %edx,%eax 613: 89 45 fc mov %eax,-0x4(%rbp) 616: 8b 45 fc mov -0x4(%rbp),%eax 619: 5d pop %rbp 61a: c3 retq 000000000000061b <main>: 61b: 55 push %rbp 61c: 48 89 e5 mov %rsp,%rbp 61f: 48 83 ec 10 sub $0x10,%rsp 623: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp) 62a: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp) 631: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 638: 8b 55 f8 mov -0x8(%rbp),%edx 63b: 8b 45 f4 mov -0xc(%rbp),%eax 63e: 89 d6 mov %edx,%esi 640: 89 c7 mov %eax,%edi 642: e8 b3 ff ff ff callq 5fa <Add> 647: 89 45 fc mov %eax,-0x4(%rbp) 64a: b8 00 00 00 00 mov $0x0,%eax 64f: c9 leaveq 650: c3 retq 651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 658: 00 00 00 65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
-
在這裡幫助了解的幾個彙編指令就是push和call以及ret。以及需要提及的幾個寄存器
- 寄存器
- eax
- ebx
- ecx
- edx
- ebp
- esp
- 其中ebp和esp這2個寄存器中存放的是位址。這2個位址是用來維護函數棧幀的。每一個函數調用都要建立一個空間。 ebp是基指針,esp是棧指針。另外%rip是程式下一條指令的位址。
- push xxx指令
- Push指令的作用就是把目前的值放到棧裡面去,然後将棧頂拓展.對應的就是将esp-1. 【這裡描述的位址是下低上高】
-
函數棧幀的建立和銷毀以及與C、C++聯系
- call指令
- call指令調用的過程
- 其中%rip 是存放程式計數器的值。 %rsp存放棧指針。
-
當調用了call之後,将%rsp往下擴充,并在新的棧幀中放入call後的指令的位址,用以傳回的時候回到這個地方。同時更改rip的值為call的值。
-
函數棧幀的建立和銷毀以及與C、C++聯系 -
函數棧幀的建立和銷毀以及與C、C++聯系
- ret指令
- 用于傳回,其對應于return。
- 作用是彈出棧頂的值指派給%rip并将%rsp回收空間
3、反彙編代碼的分析與棧幀
#include<stdio.h>
using namespace std;
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
return 0;
}
00000000000005fa <Add>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: 89 7d ec mov %edi,-0x14(%rbp)
601: 89 75 e8 mov %esi,-0x18(%rbp)
604: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
60b: 8b 55 ec mov -0x14(%rbp),%edx
60e: 8b 45 e8 mov -0x18(%rbp),%eax
611: 01 d0 add %edx,%eax
613: 89 45 fc mov %eax,-0x4(%rbp)
616: 8b 45 fc mov -0x4(%rbp),%eax
619: 5d pop %rbp
61a: c3 retq
000000000000061b <main>:
61b: 55 push %rbp
61c: 48 89 e5 mov %rsp,%rbp
61f: 48 83 ec 10 sub $0x10,%rsp
623: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp)
62a: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
631: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
638: 8b 55 f8 mov -0x8(%rbp),%edx
63b: 8b 45 f4 mov -0xc(%rbp),%eax
63e: 89 d6 mov %edx,%esi
640: 89 c7 mov %eax,%edi
642: e8 b3 ff ff ff callq 5fa <Add>
647: 89 45 fc mov %eax,-0x4(%rbp)
64a: b8 00 00 00 00 mov $0x0,%eax
64f: c9 leaveq
650: c3 retq
651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
658: 00 00 00
65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- Main函數過程調用Add前分析
-
61b: 55 push %rbp 61c: 48 89 e5 mov %rsp,%rbp 61f: 48 83 ec 10 sub $0x10,%rsp 623: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp) 62a: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp) 631: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 638: 8b 55 f8 mov -0x8(%rbp),%edx 63b: 8b 45 f4 mov -0xc(%rbp),%eax 63e: 89 d6 mov %edx,%esi 640: 89 c7 mov %eax,%edi
- 對于這部分的彙編代碼和圖的簡要解釋
- 在(61b)之前的是調用main函數之前的棧幀,main函數也是函數,它是被其他函數調用的。(再下去就要涉及os的一些知識了)
- push %rbp
- 儲存此時的%rbp值,目的是為了之後能傳回上一個調用main函數的%rbp值
- 兩個打⭐的%rbp是同一個值。
- mov %rsp,%rbp
- 是将%rbp的值賦給%rsp,此時%rsp和%rbp指向一塊位置。目的是為了新函數的管理
- sub $0x10,%rsp
- %rsp往下,實際上就是開辟main函數的棧幀空間
- %rsp和%rsp一起此時對應一個區域,能管理這塊棧幀
- movl $0xa,-0xc(%rbp)
- 給a指派10
- movl $0x14,-0x8(%rbp)
- 給b指派20
- movl $0x0,-0x4(%rbp)
- 給c指派0
- mov -0x8(%rbp),%edx
- %edx儲存b的值,準備傳參
- mov -0xc(%rbp),%eax
- %eax儲存a的值,準備傳參
- mov %edx,%esi
- 實際上這裡就是Add函數參數中的對形參的一個指派(c++中就是拷貝)
- mov %eax,%edi
- 實際上這裡就是Add函數參數中的對形參的一個指派(c++中就是拷貝)
- 可以發現,C語言中給被調函數參數指派的過程實際上是調用函數準備的。
-
- Add過程分析
-
00000000000005fa <Add>: 5fa: 55 push %rbp 5fb: 48 89 e5 mov %rsp,%rbp 5fe: 89 7d ec mov %edi,-0x14(%rbp) 601: 89 75 e8 mov %esi,-0x18(%rbp) 604: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 60b: 8b 55 ec mov -0x14(%rbp),%edx 60e: 8b 45 e8 mov -0x18(%rbp),%eax 611: 01 d0 add %edx,%eax 613: 89 45 fc mov %eax,-0x4(%rbp) 616: 8b 45 fc mov -0x4(%rbp),%eax 619: 5d pop %rbp 61a: c3 retq
- 12行的過程和main函數開始的12行的過程是一樣的。
- 同樣是為了基指針和棧指針的管理
- 3~6行則是函數内部的一個初始化
- 可以看到,此時的Add函數内部的參數存放在棧空間中。是以能了解為什麼說函數内的局部變量存放在棧空間中。
- 7~8行将參數的值複制到寄存器中進行運算,結果儲存在%eax中。【同時注意一般函數的調用參數和傳回的寄存器都是有約定俗稱的協定,如11行一般以%eax作為傳回值的寄存器】
- 9行完成運算,此時運算後的值存在%eax中
- 10行,将函數内的局部變量c的值指派為計算得到的答案
- 11行,此時将局部變量c的值(棧空間的值指派給%eax),準備傳回
- 可能有些奇怪為什麼看似多此一舉?這是對應C/C++中,值傳回的時候,是将值拷貝給一個臨時變量,再傳回的。
- 12行的pop %rbp。
- 将%rbp獲得此時棧頂指針指向的空間的内容。也就是回到main函數的時候%rbp的值
- %rsp回收空間。
- 13行的retrun。
- 程式計數器器(%rip)跳轉回Call Add指令的下一條指令的位址。
- %rsp回收空間
- 此時回到Main函數
函數棧幀的建立和銷毀以及與C、C++聯系 -
- 回到Main函數後的過程
-
647: 89 45 fc mov %eax,-0x4(%rbp) 64a: b8 00 00 00 00 mov $0x0,%eax 64f: c9 leaveq 650: c3 retq 651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 658: 00 00 00 65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 1行–将獲得傳回值指派給Main裡的c
- 2行–清空%eax
- 3行–leava
- ATT彙編的leava等價于
-
movl %ebp, %esp
popl %ebp 【32位下】
- 若為64bits 将使用rbp和rsp 寄存器
-
- 執行的是和Add函數pop的時候一樣的操作
- ATT彙編的leava等價于
- 4行retq
- 和Add函數的ret一樣,Main也是函數,也是要傳回到上一層
-
至此就完成了該程式的基本運作。
然而每個編譯器的具體處理情況略有不同。
再來看眼VS下的Add()
VS下的Add就在004516F3的位置又進行了開辟Add函數空間的操作。
而且存在對空間進行初始化rep stos。其中這也是為什麼VS用未初始化的空間一般調試出來的是燙燙燙燙燙燙。因為vs一般用該位元組碼進行空間的初始化。
4、函數棧幀的建立和銷毀與C以及C++的聯系
比如
- 函數傳參的形參拷貝
- 函數值傳回時的臨時變量
- 對被調用函數臨時變量傳址傳回或者傳引用傳回的error
- 該空間位址完全可以被其他指令覆寫,導緻錯誤輸出。