[TOC]
# 函數進棧
前面我們只是講解了一個函數的活動記錄是什么樣子的,相信大家對函數的詳細調用過程的認識還不是太清晰,這節我們就以 VS2010 Debug 模式為例來深入分析一下。
請看下面的代碼:
~~~
void func(int a, int b){
int p =12, q = 345;
}
int main(){
func(90, 26);
return 0;
}
~~~
函數使用默認的調用慣例 cdecl,即參數從右到左入棧,由調用方負責將參數出棧。函數的進棧出棧過程如下圖所示:



步驟①到⑥是函數進棧過程:
1) main() 是主函數,也需要進棧,如步驟①所示。
2) 在步驟②中,執行語句`func(90, 26);`,先將實參 90、26 壓入棧中,再將返回地址壓入棧中,這些工作都由 main() 函數(調用方)完成。這個時候 ebp 的值并沒有變,僅僅是改變 esp 的指向。
3) 到了步驟③,就開始執行 func() 的函數體了。首先將原來 ebp 寄存器的值壓入棧中(也即圖中的 old ebp),并將 esp 的值賦給 ebp,這樣 ebp 就從 main() 函數的棧底指向了 func() 函數的棧底,完成了函數棧的切換。由于此時 esp 和ebp 的值相等,所以它們也就指向了同一個位置。
4) 為局部變量、返回值等預留足夠的內存,如步驟④所示。由于棧內存在函數調用之前就已經分配好了,所以這里并不是真的分配內存,而是將 esp 的值減去一個整數,例如 esp - 0XC0,就是預留 0XC0 字節的內存。
5) 將 ebp、esi、edi 寄存器的值依次壓入棧中。
6) 將局部變量的值放入預留好的內存中。注意,第一個變量和 old ebp 之間有4個字節的空白,變量之間也有若干字節的空白。
為什么要留出這么多的空白,豈不是浪費內存嗎?這是因為我們使用Debug模式生成程序,留出多余的內存,方便加入調試信息;以Release模式生成程序時,內存將會變得更加緊湊,空白也被消除。
至此,func() 函數的活動記錄就構造完成了。可以發現,在函數的實際調用過程中,形參是不存在的,不會占用內存空間,內存中只有實參,而且是在執行函數體代碼之前、由調用方壓入棧中的。
**未初始化的局部變量的值為什么是垃圾值**
為局部變量分配內存時,僅僅是將 esp 的值減去一個整數,預留出足夠的空白內存,不同的編譯器在不同的模式下會對這片空白內存進行不同的處理,可能會初始化為一個固定的值,也可能不進行初始化。
例如在VS2010 Debug模式下,會將預留出來的內存初始化為 0XCCCCCCCC,如果不對局部變量賦值,它們的內存就不會改變,輸出時的結果就是?0XCCCCCCCC,請看下面的代碼:
~~~
#include <stdio.h>
#include <stdlib.h>
int main(){
int m, n;
printf("%#X, %#X\n", m, n);
system("pause");
return 0;
}
~~~
運行結果:
`0XCCCCCCCC, 0XCCCCCCCC `
雖然編譯器對空白內存進行了初始化,但這個值對我們來說一般沒有意義,所以我們可以認為它是垃圾值、是隨機的
# 函數出棧
步驟⑦到⑨是函數 func() 出棧過程:
7) 函數 func() 執行完成后開始出棧,首先將 edi、esi、ebx 寄存器的值出棧。
8) 將局部變量、返回值等數據出棧時,直接將 ebp 的值賦給 esp,這樣 ebp 和 esp 就指向了同一個位置。
9) 接下來將 old ebp 出棧,并賦值給現在的 ebp,此時 ebp 就指向了 func() 調用之前的位置,即 main() 活動記錄的 old ebp 位置,如步驟⑨所示。
這一步很關鍵,保證了還原到函數調用之前的情況,這也是每次調用函數時都必須將 old ebp 壓入棧中的原因。
最后根據返回地址找到下一條指令的位置,并將返回地址和實參都出棧,此時 esp 就指向了 main() 活動記錄的棧頂, 這意味著 func() 完全出棧了,棧被還原到了 func() 被調用之前的情況。
**遺留的錯誤認知**
經過上面的分析可以發現,函數出棧只是在增加 esp 寄存器的值,使它指向上一個數據,并沒有銷毀之前的數據。前面我們講局部變量在函數運行結束后立即被銷毀其實是錯誤的,這只是為了讓大家更容易理解,對局部變量的作用范圍有一個清晰的認識。
棧上的數據只有在后續函數繼續入棧時才能被覆蓋掉,這就意味著,只要時機合適,在函數外部依然能夠取得局部變量的值。請看下面的代碼:
~~~
#include <stdio.h>
int *p;
void func(int m, int n){
int a = 18, b = 100;
p = &a;
}
int main(){
int n;
func(10, 20);
n = *p;
printf("n = %d\n", n);
return 0;
}
~~~
運行結果:
`n = 18 `
在 func() 中,將局部變量 a 的地址賦給 p,在 main() 函數中調用 func(),函數剛剛調用結束,還沒有其他函數入棧,局部變量 a 所在的內存沒有被覆蓋掉,所以通過語句`n = *p;`能夠取得它的值。
- c語言
- 基礎知識
- 變量和常量
- 宏定義和預處理
- 隨機數
- register變量
- errno全局變量
- 靜態變量
- 類型
- 數組
- 類型轉換
- vs中c4996錯誤
- 數據類型和長度
- 二進制數,八進制數和十六進制數
- 位域
- typedef定義類型
- 函數和編譯
- 函數調用慣例
- 函數進棧和出棧
- 函數
- 編譯
- sizeof
- main函數接收參數
- 宏函數
- 目標文件和可執行文件有什么
- 強符號和弱符號
- 什么是鏈接
- 符號
- 強引用和弱引用
- 字符串處理函數
- sscanf
- 查找子字符串
- 字符串指針
- qt
- MFC
- 指針
- 簡介
- 指針詳解
- 案例
- 指針數組
- 偏移量
- 間接賦值
- 易錯點
- 二級指針
- 結構體指針
- 字節對齊
- 函數指針
- 指針例子
- main接收用戶輸入
- 內存布局
- 內存分區
- 空間開辟和釋放
- 堆空間操作字符串
- 內存處理函數
- 內存分頁
- 內存模型
- 棧
- 棧溢出攻擊
- 內存泄露
- 大小端存儲法
- 寄存器
- 結構體
- 共用體
- 枚舉
- 文件操作
- 文件到底是什么
- 文件打開和關閉
- 文件的順序讀寫
- 文件的隨機讀寫
- 文件復制
- FILE和緩沖區
- 文件大小
- 插入,刪除,更改文件內容
- typeid
- 內部鏈接和外部鏈接
- 動態庫
- 調試器
- 調試的概念
- vs調試
- 多文件編程
- extern關鍵字
- 頭文件規范
- 標準庫以及標準頭文件
- 頭文件只包含一次
- static
- 多線程
- 簡介
- 創建線程threads.h
- 創建線程pthread
- gdb
- 簡介
- mac使用gdb
- setjump和longjump
- 零拷貝
- gc
- 調試器原理
- c++
- c++簡介
- c++對c的擴展
- ::作用域運算符
- 名字控制
- cpp對c的增強
- const
- 變量定義數組
- 盡量以const替換#define
- 引用
- 內聯函數
- 函數默認參數
- 函數占位參數
- 函數重載
- extern "C"
- 類和對象
- 類封裝
- 構造和析構
- 深淺拷貝
- explicit關鍵字
- 動態對象創建
- 靜態成員
- 對象模型
- this
- 友元
- 單例
- 繼承
- 多態
- 運算符重載
- 賦值重載
- 指針運算符(*,->)重載
- 前置和后置++
- 左移<<運算符重載
- 函數調用符重載
- 總結
- bool重載
- 模板
- 簡介
- 普通函數和模板函數調用
- 模板的局限性
- 類模板
- 復數的模板類
- 類模板作為參數
- 類模板繼承
- 類模板類內和類外實現
- 類模板和友元函數
- 類模板實現數組
- 類型轉換
- 異常
- 異常基本語法
- 異常的接口聲明
- 異常的棧解旋
- 異常的多態
- 標準異常庫
- 自定義異常
- io
- 流的概念和類庫結構
- 標準io流
- 標準輸入流
- 標準輸出流
- 文件讀寫
- STL
- 簡介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair對組
- 深淺拷貝問題
- 使用時機
- 常用算法
- 函數對象
- 謂詞
- 內建函數對象
- 函數對象適配器
- 空間適配器
- 常用遍歷算法
- 查找算法
- 排序算法
- 拷貝和替換算法
- 算術生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各種插件
- utf8編碼
- 制作安裝項目
- 編譯模式
- 內存對齊
- 快捷鍵
- 自動補全
- 查看c++類內存布局
- FFmpeg
- ffmpeg架構
- 命令的基本格式
- 分解與復用
- 處理原始數據
- 錄屏和音
- 濾鏡
- 水印
- 音視頻的拼接與裁剪
- 視頻圖片轉換
- 直播
- ffplay
- 常見問題
- 多媒體文件處理
- ffmpeg代碼結構
- 日志系統
- 處理流數據
- linux
- 系統調用
- 常用IO函數
- 文件操作函數
- 文件描述符復制
- 目錄相關操作
- 時間相關函數
- 進程
- valgrind
- 進程通信
- 信號
- 信號產生函數
- 信號集
- 信號捕捉
- SIGCHLD信號
- 不可重入函數和可重入函數
- 進程組
- 會話
- 守護進程
- 線程
- 線程屬性
- 互斥鎖
- 讀寫鎖
- 條件變量
- 信號量
- 網絡
- 分層模型
- 協議格式
- TCP協議
- socket
- socket概念
- 網絡字節序
- ip地址轉換函數
- sockaddr數據結構
- 網絡套接字函數
- socket模型創建流程圖
- socket函數
- bind函數
- listen函數
- accept函數
- connect函數
- C/S模型-TCP
- 出錯處理封裝函數
- 多進程并發服務器
- 多線程并發服務器
- 多路I/O復用服務器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反應堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函數
- libevent
- libevent簡介