這里寫目錄標題
- 1. 內存問題
- 1.1. 內存泄漏
- 1.1.1. 內存泄漏案例檢查方法
- 1.1.2. 主線程提前退出導致【控】
- 1.1.3. PostThreadMessage失敗導致的內存泄漏**【控】**
- 1.1.4. SendMessage 時關閉客戶端【控】
- 1.1.5. 線程機制導致【**控】**
- 1.1.6. exit(0)導致【控】
- 1.2. 內存沖突
- 1.2.1. 內存非法【控】
- 1.2.2. 訪問內存沖突2【控】
- 2. 編譯階段問題
- LINK2005、1169重定義
- link1120和LNK2019:**無法解析的外部符號**
- C4996警告
- C4005宏重定義
- C2664字符集
- LNK2001 LNK1120
- 3. 文件、庫等
- 3.1. 程序的架構不一致【控】
- 4. 網絡問題
- 4.1. **服務端關閉了 socket,但客戶端未closesocket,導致新連接請求被系統拒絕**。 **10056**
- 4.2. 10054 遠程主機強迫關閉了一個現有的連接。
- 4.3. 連接失敗:10060 客戶端主動斷開連接,遠程服務端出現bug
- 5. 調試技巧
1. 內存問題
1.1. 內存泄漏
1.1.1. 內存泄漏案例檢查方法
一個很牛的方法:
-
搜索所有的new,然后在其new后打印一下new出來的內存大小,如果有delete,不確定有沒有執行delete,就在delete后打印TRACE(“xxx has been deleteed”);所有都這樣搞,再去執行一遍程序,就能發現哪個沒delete;發現控制類的Instance沒delete,就打斷點看看cHelper的構造和析構有沒有執行;
-
打斷點發現都沒執行;查找所有引用發現m_helper只聲明根本就沒實現;然后實現下,實現后就可以執行,內存泄漏的問題解決,
-
vId使用Visual Leak Detector:
- 可以得到內存泄漏點的調用堆棧,如果可以的話,還可以得到其所在文件及行號;
- 可以得到泄露內存的完整數據;
- 可以設置內存泄露報告的級別;
- 它是一個已經打包的lib,使用時無須編譯它的源代碼。而對于使用者自己的代碼,也只需要做很小的改動;
- 他的源代碼使用GNU許可發布,并有詳盡的文檔及注釋。對于想深入了解堆內存管理的讀者,是一個不錯的選
1.1.2. 主線程提前退出導致【控】
atlTraceGeneral - m_hwnd = 00000000 —說明窗口句柄未初始化
**debug輸出:**Detected memory leaks!Dumping objects ->{187} normal block at 0x0000025DAB749D20, 90 bytes long. Data:/ 61 00 74 00 6C 00 54 00 72 00 61 00 63 00 65 00
- 程序崩潰和內存轉儲(Core Dump) 程序運行時,Visual Studio 會在調試模式下自動檢測內存泄漏并輸出相關信息。輸出的內存轉儲數據包括了被泄漏的內存塊的地址、大小以及該內存區域的內容。
- Dumping objects -> 開始輸出(dump)內存中被分配的對象的詳細信息。它通常會顯示當前所有未釋放的內存塊(即潛在的內存泄漏)以及它們的大小、地址等詳細信息。
- {Object dump complete.} 一行`表示內存泄漏的對象信息已經完全輸出完畢。
- 表示一個正常的內存塊(不是數組或堆棧),它位于地址
0x0000025DAB749D20
,占用 90 字節 - 這些數據
61 00 74 00 6C 00 54 00 72 00 61 00 63 00 65 00
是 Unicode 編碼的字節表示。每對字節代表一個字符,其中每個字符占 2 字節。 轉換成可讀的字符就是:a t l T r a c e
。
內存泄漏信息表示在程序運行中分配了內存,但未正確釋放。常見原因:
- 對象未被銷毀,例如對話框對象未調用
DestroyWindow
。 - 動態分配的資源未釋放。
問題原因:
_beginthread
并不提供線程同步功能,它只是啟動一個線程并管理資源 ,導致主線程開啟這個線程后一會主線程執行完結束了,導致程序退出,導致線程強制終止,導致創建的內存還沒來得及釋放。
解決
- sleep主動阻塞主線程
- 用thread+join √
1.1.3. PostThreadMessage失敗導致的內存泄漏**【控】**
雖然在SendPack那個線程收到這個new的包復制后就立馬delete,但是萬一發送失敗,就會產生內存泄漏,所以在這地方也要作出處理,那失敗的包怎么處理???
1.1.4. SendMessage 時關閉客戶端【控】
服務端在遠程啟動,關閉客戶端后發現很多鼠標和圖像的內存泄漏,因為客戶端SendPack收到數據后,SendMessage到dlg是new的數據發過去的,如果此時客戶端關閉了,這些new的數據無法釋放,本來處理邏輯是在dlg收到數據后復制一下(棧),然后吧堆區數據給釋放;這種情況怎么辦???
1.1.5. 線程機制導致【控】
線程函數,不管是主線程子線程,對cpu的使用權,記錄一個使用權,main結束是回到系統函數,真正的啟動函數點事mainCRTStartip,調用到main了;
當使用 endthread
來結束線程時,它會直接終止線程,并且不允許線程中的局部變量正常析構(坑的點)。這可能導致內存泄漏或無法釋放的資源,因為局部變量的析構函數不會被調用。 就會導致內存泄漏,現在把這個線程函數的主題放入到另一個函數threadmain里,這樣函數結束就會析構變量,所以一般線程函數里在搞一個函數;線程入口函數(threadQueueEntry)和線程功能函數(threadmain)分開;
解決思路:
- 先把所有的new搜一遍,對應的delete是否能對應上。
- 確保delete是否執行到?打印日志一般,一般到這問題就出現了
- 現在發現delete對沒問題,唯一的可能是list, 當你往
std::list<T>
里添加元素時,每個節點通常都是 在堆區分配的,而std::list
本身的管理結構體(如begin
指針、end
指針等)可能位于棧上或堆上,取決于std::list
的 定義方式。 - 試著調用list.clear,仍然泄露
- 最后百度搜索_endthread的問題,解決方案在上面
1.1.6. exit(0)導致【控】
內存泄漏往往很難定位觸發點,此次情況,new delete對應ok;加日志,所有可能內存泄漏地方加日志分析;
之前這里有個exit(0);函數直接終止,不會 調用當前作用域中的局部變量的析構函數(包括棧上的對象) ,圖中創建的隊列也只會調用構造,不對調用其析構,就導致內存泄漏;和那個endthread是一樣道理;
去掉這個exit后,內存泄漏沒了,但是出現了崩潰的bug;
1.2. 內存沖突
1.2.1. 內存非法【控】
在使用 memcpy
之前,需要確保以下幾點:
- 確保
**strData**
的大小足夠: 調用resize(nSize)
后,strData
的大小應該足夠容納nSize
字節。 - 確保
**pData**
是有效的指針: 在調用memcpy
前,檢查pData
是否為空指針。 - 確保
**nSize**
合法: 確保nSize
在合理范圍內,既不能為負值,也不能超過pData
實際擁有的內存大小。
發現:
pData
的值為 0x00000009
,這是一個非常不合理的指針地址(通常有效的指針應該是內存中有效的區域,如堆或棧上的地址),并且調試器提示 讀取字符串時出錯。這說明問題的根本原因是 pData
是一個非法指針。
通過調用堆棧,查看調用這個函數的地方:
發現data是9,傳入的參數,把一個非指針( 整數值
9
)轉為指針,(BYTE*)data
并不是指向有效內存區域的指針,而是一個直接指向地址 0x00000009
的指針,這顯然是非法的。
經驗: 所以以后看到0x00000009
這樣的非法指針,很有可能是把一個數據強轉為指針然后傳了。
經驗2:報錯系統庫函數里的變量內存訪問沖突,發現其內存是0x000005b8,這顯然是個不合法內存;
這種情況就看調用堆棧,觀察自己的代碼哪里開始出錯;發現在控制類的InitController里的this指針是空指針;
合法內存地址通常看起來是較大的十六進制值
常見的非法地址包括:
0x00000000
:空指針。0x000005b8
:屬于操作系統保護的低地址區域。0xdeadbeef
或0xcccccccc
:調試環境中常用的未初始化指針標記。- 非法越界地址,例如訪問數組時超出范圍。
1.2.2. 訪問內存沖突2【控】
四個參數里,其他三個是上面定義的局部變量,肯定沒問題,那問題只能處在成員變量m_hCompletionPort,調用堆棧向前分析:
發現thiz指針非法;那就是線程函數傳遞進來的參數有問題;
傳錯了,應該傳的this指針,才能去調用其成員變量;問題根源是直接ctrl c過來的,現在把這些東西都封裝在類里面,線程函數傳的參數也應該改變!
2. 編譯階段問題
-
遇到編譯問題不要慌,一個一個解決
-
解決的方法:
-
調整頭文件的順序(宏沖突)(
windows.h
的宏污染(如min
/max
)或 C++ 標準庫沖突)22222222222222 -
對應的庫文件的引用(
LNK2019
無法解析的外部符號錯誤) -
#pragma comment(lib, “rpcrt4.lib”) // 顯式鏈接庫
-
4996 的警告一般可以通過警告本身的提示,添加宏來解決
-
#define _CRT_SECURE_NO_WARNINGS // 禁用安全警告
-
頭文件引用錯誤(若無法跳轉到定義,說明路徑錯誤或文件缺失)
調試問題總結:
1 socket 返回-1 而參數沒有問題,那么有可能是 WSAStartup 未調用
2 很多時候,問題是疊加在一起的,只要不斷的解決,總能搞定
3 沒有測試的程序,是不靠譜的,無論誰寫的
4 大膽假設,小心求證
LINK2005、1169重定義
其實就是在頭文件聲明和定義了函數,在多個.cpp文件#include這個頭文件就會報錯,出個定義,違反了ODR原則。
解決辦法
- 函數聲明與定義分離,聲明在頭文件,定義在 .cpp 文件
- 將函數聲明為
**inline**
, 如果你需要在頭文件中定義函數(例如,函數實現非常簡單),可以將其聲明為inline
,讓編譯器將其視為內聯函數,不會重復定義: 注意:inline
適用于小型、簡單的函數。如果函數較大或復雜,建議不要內聯。 - 將函數聲明為 全局static,使其作用域限制在當前編譯單元(每個 .cpp 文件生成獨立副本)
- 定義為一個類的靜態函數
link1120和LNK2019:無法解析的外部符號
1、無法解析的外部符號,基本上都是聲明了函數,但是沒有定義這個函數
LNK2019
無法解析的外部符號 _main,函數 “int __cdecl invoke_main(void)” (invoke_main@@YAHXZ) 中引用了該符號
-
這通常意味著鏈接器找不到程序的入口點
main
函數 -
未定義main函數
-
展開 鏈接器 > 系統,查看 子系統 選項:
-
如果您正在寫控制臺程序,確保子系統設置為 Console (/SUBSYSTEM:CONSOLE)。
-
如果是 Windows 應用程序,確保子系統設置為 Windows (/SUBSYSTEM:WINDOWS)。
-
根據項目類型,實現相應的入口函數:
-
對于控制臺程序,定義
main
函數。 -
對于 Windows 程序,定義
WinMain
函數
發現是入口點函數寫成了main,但是客戶端mfc項目是以winmain為起點的
2、 RTSPServer.obj : error LNK2019: 無法解析的外部符號 __imp**__UuidCreate@4,函數 “public: __thiscall RTSPSession::RTSPSession(class ESocket const &)**” (??0RTSPSession@@QAE@ABVESocket@@@Z) 中引用了該符號 :
- 通常是由于 未正確鏈接 RPC 運行時庫 或 函數聲明與實現不匹配 導致的
UuidCreate
是 Windows RPC 運行時庫中的函數,其聲明位于rpcdce.h
或rpc.h
,實現位于rpcrt4.lib
。若未正確鏈接該庫,或未包含頭文件,會引發此錯誤- 解決:
#pragma comment(lib, "rpcrt4.lib") // 添加此行到引用 UuidCreate 的源文件或頭文件中
或在項目屬性中配置鏈接庫:
- 右鍵項目 → 屬性 → 鏈接器 → 輸入 → 附加依賴項 → 添加
rpcrt4.lib
2 6。 - 確保
rpcrt4.lib
的路徑正確(如C:\Program Files (x86)\Windows Kits\10\Lib\...
)
缺網絡庫:#pragma comment(lib, “ws2_32.lib”)
3、
- 報錯:
- 此時頭文件目錄,靜態庫都配到 vs 項目里,dll 也配到環境變量
- 發現這些庫是 release 版本,vs 使用 debug 模式,改為 release 后,報錯變化為下:
C4996警告
C4996 是 Visual Studio 編譯器的一個警告代碼,用于指示一些被 Microsoft 標記為不安全或已過時的函數。例如scanf等sprintf
,inet_addr
函數已被視為過時。推薦使用 inet_pton()
項目里面禁用4996警告三種方法:
禁用SDL檢查Security Development Lifecycle Checks)可以解決;不推薦長期禁用 SDL 檢查:SDL 檢查的目的是幫助開發者編寫更安全的代碼。
- 項目屬性里編譯器預處理那定義,在這里加上去個 _WINSOCK_DEPRECATED_NO_WARNINGS; 來禁用與過時 API 相關的警告
- 文件內部定義#pragma warning(disable:4996)
C4005宏重定義
- 頭文件順序問題:
windows.h
文件在 Windows 平臺中定義了大量的宏和類型,這些宏可能與套接字頭文件中的定義沖突。- 例如,
windows.h
可能會定義min
和max
宏,與其他頭文件中的定義沖突。 - 解決:將套接字的頭文件(如
winsock2.h
或sys/socket.h
)放在windows.h
前面
C2664字符集
解決方法:vs字符集改為多字符集即可
LNK2001 LNK1120
- LNK1120:共9個未解析的外部符號(函數/變量)
- LNK2001:具體列出缺失的FFmpeg API:
問題類型 | 具體表現 | 典型場景 |
---|---|---|
庫文件未鏈接 | 缺少avformat.lib 等靜態庫 | 項目配置遺漏 |
函數聲明不匹配 | 頭文件與庫版本不一致 | 升級FFmpeg后未更新頭文件 |
編譯架構沖突 | x86庫用于x64項目 | 平臺配置錯誤 |
當C++代碼調用C語言庫(如FFmpeg)時,必須使用此語法包裹C語言的頭文件。
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
}
- C++ 語言支持函數重載等特性,編譯器在編譯時會對函數名進行 “名字改編(Name Mangling)”,將函數名和參數類型等信息混合生成內部使用的符號名,以此區分同名但參數不同的函數。而 C 語言不支持函數重載,沒有名字改編機制 ,函數名就是實際鏈接時使用的符號名。
- FFmpeg 庫(
libavcodec
和libavformat
是其相關庫)是用 C 語言編寫的,為了能在 C++ 代碼中(.cpp文件里)正確調用 FFmpeg 庫中的 C 函數,需要告訴 C++ 編譯器,這部分代碼要按照 C 語言的規則進行編譯,避免名字改編帶來的鏈接錯誤,extern "C"
就是起這個作用。
3. 文件、庫等
3.1. 程序的架構不一致【控】
一個典型的 Windows 錯誤,錯誤代碼為 0xc000007b
,提示程序無法正常啟動 :
- 確保程序的架構(32 位或 64 位)與所有依賴的 DLL 的架構一致。
發現正是這個問題導致!
4. 網絡問題
recv
返回 -1 時的錯誤碼
-
**WSAECONNRESET**
(10054): -
服務端主動關閉了連接,導致客戶端讀取數據失敗。
-
或者,服務端異常退出,連接被 TCP 重置。
-
**WSAETIMEDOUT**
(10060): -
接收超時,客戶端等待服務端的數據時超時。
-
**WSAENOTCONN**
(10057): -
套接字未連接,可能是客戶端在連接斷開后嘗試接收數據。
-
**WSAEINTR**
: -
接收操作被中斷。
4.1. 服務端關閉了 socket,但客戶端未closesocket,導致新連接請求被系統拒絕。 10056
10056 在一個已經連接的套接字上做了一個連接請求
1、發現客戶端也closesocket之后再connect就能連上了,這是為啥????
-
**closesocket**
釋放了客戶端的本地端口 -
當客戶端調用
closesocket()
時,本地的套接字資源被立即釋放,包括綁定的本地端口。 -
此時,客戶端可以立即創建一個新的套接字,并發起
connect
操作。 -
因為新的套接字使用了新的資源,與之前的舊套接字無關,因此不會有沖突。
-
服務端的端口可以接受新的連接
-
服務端雖然進入了
TIME_WAIT
狀態,但TIME_WAIT
并不影響服務端套接字接受新的連接請求。 -
TIME_WAIT
只會限制服務端重用之前的連接,而不會阻止服務端監聽套接字接受新的連接。 -
新的套接字使用了新的連接三元組
-
每個 TCP 連接由三元組
(客戶端 IP, 客戶端端口, 服務端 IP:服務端端口)
唯一標識。 -
當客戶端創建新的套接字時,系統會為其分配一個新的客戶端端口(如果沒有指定),使新的連接三元組和舊的連接三元組完全獨立,從而不會沖突。
-
網絡資源釋放及時
-
客戶端調用
closesocket()
時,系統通常會立即釋放該套接字的本地端口資源,而無需等待TIME_WAIT
。
2、如果客戶端等待 2 分鐘(或者操作系統配置的 TIME_WAIT
時間)讓服務端的 TIME_WAIT
狀態過去后, 客戶端在 不調用 **closesocket()**
的情況下,直接使用同一個套接字句柄 (socket
) 再次調用 connect
,這種行為是 非法的,并且會導致 **connect**
調用失敗,報錯 **WSAEISCONN**
或 **WSAENOTSOCK**
,具體原因如下:
-
**socket**
的狀態已經是已連接 -
在 TCP 協議中,當客戶端完成一次
connect
后,該套接字已經綁定到特定的連接三元組(本地IP, 本地端口, 服務端IP:服務端端口)
。 -
套接字資源仍然有效,但它已經與之前的連接綁定。
-
如果此時服務端已經關閉連接,而客戶端沒有關閉套接字,卻嘗試再次調用
connect
: -
操作系統會檢查套接字的狀態,發現它仍然處于連接綁定狀態,而不是一個空閑的套接字。
-
此時調用
connect
會報錯**WSAEISCONN**
(表示該套接字已連接)。 -
服務端的
**TIME_WAIT**
不影響這個行為 -
服務端的
TIME_WAIT
狀態與客戶端無關。客戶端的行為取決于其套接字資源的狀態。 -
如果客戶端的套接字仍然有效,但已經綁定到舊的三元組,則不能直接使用該套接字重新連接。
-
客戶端的操作系統規則
-
操作系統對 TCP 套接字的狀態有嚴格的定義,一個已連接的套接字不能被用于發起新的連接。
-
TCP 套接字的生命周期如下:
**socket()**
:創建套接字,分配資源。**connect()**
:與服務端建立連接,綁定三元組。**closesocket()**
:關閉套接字,釋放綁定的三元組。
- 如果沒有調用
closesocket()
,操作系統仍然認為該套接字與之前的連接綁定,即使服務端關閉連接,客戶端也無法重新使用該套接字。
4.2. 10054 遠程主機強迫關閉了一個現有的連接。
本機聯調進行testConnection通信沒問題,但是服務端放在虛擬機上,就報這個報錯!
已知錯誤碼10054
-
**WSAECONNRESET**
(10054): -
服務端主動關閉了連接,導致客戶端讀取數據失敗。
-
或者,服務端異常退出,連接被 TCP 重置。
難繃,客戶端第一次connect服務端返回成功,send也沒報錯,recv報錯了,但是發現ping不通虛擬機,那為啥客戶端第一次connect會返回0呢;ping不同是因為主機換wifi了,網段變了,但之前虛擬機設置了靜態ip,橋接模式下虛擬機和宿主機網段一樣才能ping通,
改了ip后,CS網絡通信正常了!
可能回答:
虛擬機的 IP 靜態配置沒有影響初始連接
- 雖然虛擬機設置了靜態 IP,但當宿主機切換 Wi-Fi 導致網段改變時,虛擬機和宿主機可能仍然在某種程度上保持通信能力。
- 原因:在某些橋接模式下,虛擬機和宿主機可能會嘗試使用舊的 ARP 緩存或其他機制進行通信,允許一部分數據包通過,尤其是初始的 TCP 握手。
- 但后續的數據包可能因網段不一致而丟失。
為什么 **send**
沒報錯?
- TCP 的發送緩沖區行為 當客戶端調用
send
時,數據會先被放入操作系統的發送緩沖區,立即返回成功。 - 如果 TCP 的狀態仍然是
ESTABLISHED
,操作系統不會立即檢查數據是否成功到達服務端。 - 即使
send
成功,數據可能無法到達服務端,因為網段不一致或路由配置錯誤導致數據包被丟棄。 - 當客戶端調用
recv
時,操作系統檢測到連接被服務端關閉,返回錯誤碼10054
。
為什么 recv
報錯?
- 由于虛擬機的 IP 地址錯誤,服務端可能接收不到客戶端發送的數據,誤認為客戶端超時,主動關閉了連接。
- 服務端關閉連接后,客戶端的
recv
會報錯10054
,表示連接被重置。
4.3. 連接失敗:10060 客戶端主動斷開連接,遠程服務端出現bug
1 由于連接方在一段時間后沒有正確答復或連接的主機沒有反應,連接嘗試失敗
為
LISTENING
狀態僅表示服務端已經調用了 listen
并正在監聽指定端口。
如果套接字是默認的阻塞模式,調用 recv
時:
- 如果接收緩沖區中有數據,
recv
會立即返回已接收的數據字節數。 - 如果緩沖區中沒有數據,
recv
會阻塞當前線程,直到:
- 數據可用。
- 連接被對端關閉(返回
**0**
)。或者設置接受0長度那也返回0; 當服務端的recv
函數返回0
時,表示 對端(客戶端)已經優雅地關閉了連接,即發送了一個 FIN 包。對于這種情況:recv
返回0
是一次性的。 如果繼續調用recv
,通常會返回**-1**
,并設置錯誤碼(如EINVAL
或類似錯誤),提示該套接字已失效。 - 發生錯誤(返回
-1
)。
問題:本機聯調客戶端和服務器沒問題,服務端放遠程后一直啟動,客戶端第一次啟動也沒問題,但第二次啟動發現服務端在dealcommand里死循環,無法再次進入accept函數里導致客戶端connect時候報錯10060;
**那為啥死循環?
**:發現多次客戶端測試網絡通信,服務端accept返回的m_client都是552???總不能每次都復用552吧;
客戶端這邊每次發送新的命令,使用的socket會變,只是偶爾復用原來的socket;
死循環是因為客戶端關閉后,應該要return后重新accept新的客戶端,但代碼邏輯還一直在recv recv循環,邏輯問題;
為什么 **recv**
會一直頻繁返回 **0**
?
當 recv
返回 0
時,說明對端(客戶端)已經優雅地關閉了連接(發送了 FIN 包)。但在 **recv**
返回 **0**
之后,套接字仍然是合法的(并沒有直接closesocket,才會發fin)還可以收數據,因此你可以繼續對這個套接字調用 recv
,而它仍然可能返回 0
。
- 阻塞行為在這種情況下不適用,因為套接字已經進入了一種特殊狀態(“對端關閉但本端未關閉”),所以
recv
不會再阻塞。 - 如果服務端繼續調用
recv
,TCP 協議層會立即返回0
,表示對端已經關閉發送方向。
為什么沒有返回 **-1**
?
**-1**
是錯誤的標志,而不是連接關閉的標志:
- 當發生網絡錯誤或套接字非法時 ,
recv
返回-1
。 - 對于對端優雅關閉的情況(發送了 FIN 包),
recv
不會返回-1
,因為這是正常的 TCP 關閉行為,不是錯誤。 - 阻塞行為在這種情況下不適用,因為套接字已經進入了一種特殊狀態(“對端關閉但本端未關閉”),所以
recv
不會再阻塞。
解決:
直接return,客戶端都斷開了還while個屁,需要return然后服務端再次進入accept阻塞等待新的客戶端連接;
但是為啥本機調試時候沒出現這個問題呢:
本機調試,客戶端關閉時候,服務端肯定也是一起關閉的,死循環雖有但是關閉后看不到了罷了;
5. 調試技巧
- send()之后,如果想知道send多少數據,直接在寄存器eax里查看就行/
InitSocket執行時候出問題了,可以右鍵這個函數:設置下一條語句,直接進去走一遍檢查
- 斷點中加條件,例如在循環里執行到某次時候斷點,或者遍歷數據集時候的某個數據打斷點
或者這個循環命中次數
- 調用堆棧窗口的重要功能是:可以找到當前函數的調用函數,以及依次往前的每一級調用函數。