深入解析函數棧幀創建與銷毀

目錄

一、函數棧幀(Stack Frame)整理

1、核心概念

2、為什么需要函數棧幀?

3、函數棧幀的主要內容

二、理解函數棧幀能解決的核心問題

1、局部變量的生命周期與本質

2、函數調用的參數傳遞機制

3、函數返回值的傳遞

三、函數棧幀的創建和銷毀解析

1、什么是棧(Stack)?

2、認識相關寄存器和匯編指令

a) 相關寄存器

b) 相關匯編指令

3、解析函數棧幀的創建和銷毀

1. 預備知識

2. 函數的調用堆棧

3.?準備環境

4. 轉到反匯編

5. 函數棧幀的創建

小知識:燙燙燙~

Add函數的傳參

函數調用過程

6. 函數棧幀的銷毀

拓展了解


一、函數棧幀(Stack Frame)整理

1、核心概念

????????函數棧幀(也稱為活動記錄)是函數被調用時,在程序的調用棧(Call Stack)?上為其分配的一塊內存空間。它用于支持函數的執行和管理函數調用過程。

2、為什么需要函數棧幀?

????????C程序以函數為基本單位。當一個函數調用另一個函數時,需要解決以下幾個關鍵問題,而函數棧幀正是解決這些問題的機制:

  • 函數參數如何傳遞?

  • 函數內部的局部變量如何存儲?

  • 函數調用結束后,如何返回到正確的位置繼續執行?

  • 函數的返回值如何傳遞給調用者?

  • 函數執行前后,如何保證調用者寄存器的狀態不被破壞?

3、函數棧幀的主要內容

一塊函數棧幀通常包含以下幾類信息:

  • 函數參數與返回值:存儲傳遞給被調用函數的參數以及函數返回時的返回值。

  • 臨時變量

    • 函數的非靜態局部變量

    • 編譯器自動生成的其他臨時變量

  • 上下文信息

    • 調用函數的返回地址(調用指令下一條指令的地址)。

    • 調用函數的棧幀基地址,用于在當前函數返回后恢復調用者的棧幀。

    • 為保持函數調用前后不變而需要保存的寄存器值


二、理解函數棧幀能解決的核心問題

????????深入理解函數棧幀的創建和銷毀過程,就像是獲得了C語言函數底層工作機制的“地圖”,許多令人困惑的語法現象和編程難題都會變得清晰明了。具體來說,它能幫助我們徹底理解以下關鍵問題:

1、局部變量的生命周期與本質

  • 局部變量是如何創建的?:它們并非憑空產生,而是在其所屬函數的棧幀被創建時,在棧上分配了內存空間。所謂的“創建”,就是移動棧指針來預留一塊足夠大的內存。

  • 為什么局部變量不初始化內容是隨機的?:因為“創建”僅僅是在棧上分配了空間,而這片空間之前很可能被其他函數使用過,殘留著之前的數據。如果不主動初始化(賦值),直接使用該內存的值,看到的自然就是不可預測的“隨機值”或“垃圾值”。

2、函數調用的參數傳遞機制

  • 函數調用時參數是如何傳遞的?:通常不是在被調用函數的棧幀里直接“變出”參數。而是由調用者將自己的實參(的值或地址)壓入棧中(或存入約定的寄存器)。隨后,被調用函數才能在自己的棧幀中找到這些參數。

  • 傳參的順序是怎樣的?:理解棧幀可以清楚地看到,參數通常是從右向左依次壓入棧中(超重要!!!)。這是為了支持像printf這樣的可變參數函數。

  • 形參和實參的關系形參其實就是函數棧幀中為參數預留的位置。在函數被調用時,實參的值會被拷貝(復制)?到形參所在的內存中。因此,形參是實參的一份副本,修改形參(在大多數情況下)不會影響實參,這解釋了為何值傳遞是有效的。

3、函數返回值的傳遞

  • 函數的返回值是如何帶回的?:通常,返回值不會通過棧幀的主要部分傳遞。而是通過一個特定的寄存器(如eax/rax)來存儲并帶回給調用者。如果返回值較大,可能會采用調用者預先分配空間并傳入地址等其他機制。

????????總結而言,理解函數棧幀就是將編程語言中“函數調用”這個抽象概念,轉化為CPU和內存中“分配空間、拷貝數據、跳轉指令、恢復現場”等一系列具體操作的過程。這是連接高級語言語法和計算機底層邏輯的關鍵橋梁。讓我們一同深入分析函數棧幀創建和銷毀的詳細過程。


三、函數棧幀的創建和銷毀解析

1、什么是棧(Stack)?

????????棧是現代計算機程序中一個至關重要的基礎概念。它支撐著函數調用、局部變量管理等核心功能,可以說沒有棧,就沒有我們現在看到的高級編程語言。

棧被定義為一個遵守?“后進先出”(Last In First Out, LIFO)?原則的特殊容器。

  • 操作:數據可以壓入(Push)?棧頂,也可以從棧頂彈出(Pop)

  • 規則:最先壓入的數據最后彈出,最后壓入的數據最先彈出。(類比:疊放的盤子,總是取最上面的那個,最下面的盤子是最后才能被取到的)。

在計算機系統的具體實現中:

  • 棧是一塊動態的內存區域

  • 壓棧(Push)?使棧增大,出棧(Pop)?使棧減小。

  • 在經典的操作系統(如i386, x86-64架構)中,棧的增長方向是向下的,即從高地址低地址擴展。

  • 棧頂的位置由一個名為?esp?(Stack Pointer) 的專用寄存器來定位和跟蹤。

2、認識相關寄存器和匯編指令

理解函數棧幀的操作需要了解一些關鍵的底層硬件寄存器和匯編指令。

a) 相關寄存器

寄存器全稱與用途
eax通用寄存器,通常用于存儲臨時數據和函數的返回值
ebx通用寄存器,用于保留臨時數據。
ebp棧底指針寄存器 (Base Pointer),用于定位當前函數棧幀的底部。在函數執行過程中,其值通常保持穩定,從而可以通過ebp方便地訪問參數和局部變量。
esp棧頂指針寄存器 (Stack Pointer),始終指向系統棧的最頂部(下一個可用的最低地址)pushpop操作都會直接改變esp的值。
eip指令指針寄存器 (Instruction Pointer),保存著CPU下一條要執行的指令的地址。程序的執行流程就是由eip的指向決定的。

b) 相關匯編指令

匯編指令功能描述
mov數據轉移指令。例如?mov eax, ebx?將ebx的值拷貝到eax中。
push數據入棧。1. 先將esp的值減小(棧向下增長)。2. 再將數據寫入新的棧頂地址。
pop數據出棧。1. 先將esp指向的數據讀出來。2. 再將esp的值增加(棧收縮)。
sub減法指令。常用于減小esp的值來為函數局部變量開辟空間。例如?sub esp, 0Ch
add加法指令。常用于增加esp的值來回收函數局部變量的空間。例如?add esp, 0Ch
call函數調用指令。它主要做兩件事:
1.?壓入返回地址:將call指令的下一條指令的地址壓入棧中。
2.?轉入目標函數:修改eip,開始執行被調用函數的代碼。
jump跳轉指令。通過直接修改eip寄存器的值,來改變程序的執行流程。
ret函數返回指令。它的作用類似于?pop eip,即call指令壓入棧的返回地址彈出,并放入eip寄存器中,從而使程序跳回到調用者函數中繼續執行。

3、解析函數棧幀的創建和銷毀

1. 預備知識

首先我們達成一些預備知識才能有效的幫助我們理解,函數棧幀的創建和銷毀。

  • 每一次函數調用,都要為本次函數調用開辟空間,就是函數棧幀的空間。
  • 這塊空間的維護是使用了2個寄存器: esp 和 ebp , ebp 記錄的是棧底的地址, esp 記錄的是棧頂的地址。

如圖所示:

  • 函數棧幀的創建和銷毀過程,在不同的編譯器上實現的方法大同小異,本次演示以VS2019為例。

2. 函數的調用堆棧

演示代碼:

#include <stdio.h>int Add(int x, int y) 
{int z = 0;z = x + y;return z;
}int main() 
{int a = 3;int b = 5;int ret = 0;ret = Add(a, b);printf("%d\n", ret);return 0;
}

????????這段代碼,如果我們在VS2019編譯器上調試,調試進入Add函數后,我們就可以觀察到函數的調用堆棧 (右擊勾選【顯示外部代碼】),如下圖:

????????函數調用堆棧是反饋函數調用邏輯的,那我們可以清晰的觀察到, main 函數調用之前,是由 invoke_main 函數來調用main函數。 在 invoke_main 函數之前的函數調用我們就暫時不考慮了。那我們可以確定, invoke_main 函數應該會有自己的棧幀, main 函數和 Add 函數也會維護自己的棧幀,每個函數棧幀都有自己的 ebp 和 esp 來維護棧幀空間。 那接下來我們從main函數的棧幀創建開始講解:

3.?準備環境

????????為了讓我們研究函數棧幀的過程足夠清晰,不要太多干擾,我們可以關閉下面的選項,讓匯編代碼中排除一些編譯器附加的代碼,首先右擊“解決方案”欄中的項目,打開如下,跟著步驟來:

4. 轉到反匯編

????????調試到main函數開始執行的第一行,右擊鼠標找到“反匯編”選項并點擊,轉到反匯編。 注:VS編譯器每次調試都會為程序重新分配內存,博客中的反匯編代碼是一次調試代碼過程中數據,每次調試略有差異。

int main()
{
//函數棧幀的創建
00BE1820 ?push ? ? ? ?ebp ?
00BE1821 ?mov ? ? ? ? ebp,esp ?
00BE1823 ?sub ? ? ? ? esp,0E4h ?
00BE1829 ?push ? ? ? ?ebx ?
00BE182A ?push ? ? ? ?esi ?
00BE182B ?push ? ? ? ?edi ?
00BE182C ?lea ? ? ? ? edi,[ebp-24h] ?
00BE182F ?mov ? ? ? ? ecx,9 ?
00BE1834 ?mov ? ? ? ? eax,0CCCCCCCCh ?
00BE1839 ?rep stos ? ?dword ptr es:[edi] ?
//main函數中的核心代碼int a = 3;
00BE183B ?mov ? ? ? ? dword ptr [ebp-8],3 ?int b = 5;
00BE1842 ?mov ? ? ? ? dword ptr [ebp-14h],5 ?int ret = 0;
00BE1849 ?mov ? ? ? ? dword ptr [ebp-20h],0 ?ret = Add(a, b);
00BE1850 ?mov ? ? ? ? eax,dword ptr [ebp-14h] ?
00BE1853 ?push ? ? ? ?eax ?
00BE1854 ?mov ? ? ? ? ecx,dword ptr [ebp-8] ?
00BE1857 ?push ? ? ? ?ecx ?
00BE1858 ?call ? ? ? ?00BE10B4 ?
00BE185D ?add ? ? ? ? esp,8 ?
00BE1860 ?mov ? ? ? ? dword ptr [ebp-20h],eax ?printf("%d\n", ret);
00BE1863 ?mov ? ? ? ? eax,dword ptr [ebp-20h] ?
00BE1866 ?push ? ? ? ?eax ?
00BE1867 ?push ? ? ? ?0BE7B30h ?
00BE186C ?call ? ? ? ?00BE10D2 ?
00BE1871 ?add ? ? ? ? esp,8 ?return 0;
00BE1874 ?xor ? ? ? ? eax,eax ?
}

5. 函數棧幀的創建

這里看到 main 函數轉化來的匯編代碼如上所示。 接下來我們就一行行拆解匯編代碼

00BE1820 ?push ? ? ? ?ebp ? ?//把ebp寄存器中的值進行壓棧,此時的ebp中存放的是invoke_main函數棧幀的ebp,esp-4
00BE1821 ?mov ? ? ? ? ebp,esp ?//move指令會把esp的值存放到ebp中,相當于產生了main函數的ebp,這個值就是invoke_main函數棧幀的esp
00BE1823 ?sub ? ? ? ? esp,0E4h ?//sub會讓esp中的地址減去一個16進制數字0xe4,產生新的esp,此時的esp是main函數棧幀的esp,此時結合上一條指令的ebp和當前的esp,ebp和esp之間維護了一個塊棧空間,這塊棧空間就是為main函數開辟的,就是main函數的棧幀空間,這一段空間中將存儲main函數中的局部變量,臨時數據已經調試信息等。
00BE1829 ?push ? ? ? ?ebx ?//將寄存器ebx的值壓棧,esp-4
00BE182A ?push ? ? ? ?esi ?//將寄存器esi的值壓棧,esp-4
00BE182B ?push ? ? ? ?edi ?//將寄存器edi的值壓棧,esp-4
//上面3條指令保存了3個寄存器的值在棧區,這3個寄存器的在函數隨后執行中可能會被修改,所以先保存寄存器原來的值,以便在退出函數時恢復。//下面的代碼是在初始化main函數的棧幀空間。
//1. 先把ebp-24h的地址,放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 將從edp-0x2h到ebp這一段的內存的每個字節都初始化為0xCC
00BE182C ?lea ? ? ? ? edi,[ebp-24h] ?
00BE182F ?mov ? ? ? ? ecx,9 ?
00BE1834 ?mov ? ? ? ? eax,0CCCCCCCCh ?
00BE1839 ?rep stos ? ?dword ptr es:[edi]

上面的這段代碼最后4句,等價于下面的偽代碼:

edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{*(int*)edi = eax;
}

小知識:燙燙燙~

????????之所以上面的程序輸出“燙”這么一個奇怪的字,是因為main函數調用時,在棧區開辟的空間的其中每一 個字節都被初始化為0xCC,而arr數組是一個未初始化的數組,恰好在這塊空間上創建的,0xCCCC(兩 個連續排列的0xCC)的漢字編碼就是“燙”,所以0xCCCC被當作文本就是“燙”。接下來我們再分析main函數中的核心代碼:

 int a = 3;
00BE183B ?mov ? ? ? ? dword ptr [ebp-8],3 ?//將3存儲到ebp-8的地址處,ebp-8的位置其實就是a變量int b = 5;
00BE1842 ?mov ? ? ? ? dword ptr [ebp-14h],5 //將5存儲到ebp-14h的地址處,ebp-14h的位置其實是b變量int ret = 0;
00BE1849 ?mov ? ? ? ? dword ptr [ebp-20h],0 ?//將0存儲到ebp-20h的地址處,ebp-20h的位置其實是ret變量
//以上匯編代碼表示的變量a,b,ret的創建和初始化,這就是局部的變量的創建和初始化
//其實是局部變量的創建時在局部變量所在函數的棧幀空間中創建的
//調用Add函數ret = Add(a, b);
//調用Add函數時的傳參
//其實傳參就是把參數push到棧幀空間中
00BE1850 ?mov ? ? ? ? eax,dword ptr [ebp-14h] ?//傳遞b,將ebp-14h處放的5放在eax寄存器中
00BE1853 ?push ? ? ? ?eax ? ? ? ? ? ? ? ? ? ? ?//將eax的值壓棧,esp-4
00BE1854 ?mov ? ? ? ? ecx,dword ptr [ebp-8] ? ?//傳遞a,將ebp-8處放的3放在ecx寄存器中
00BE1857 ?push ? ? ? ?ecx ? ? ? ? ? ? ? ? ? ? ?//將ecx的值壓棧,esp-4
//跳轉調用函數
00BE1858 ?call ? ? ? ?00BE10B4 ?
00BE185D ?add ? ? ? ? esp,8 ?
00BE1860 ?mov ? ? ? ? dword ptr [ebp-20h],eax

Add函數的傳參
//調用Add函數ret = Add(a, b);
//調用Add函數時的傳參
//其實傳參就是把參數push到棧幀空間中,這里就是函數傳參
00BE1850 ?mov ? ? ? ? eax,dword ptr [ebp-14h] ?//傳遞b,將ebp-14h處放的5放在eax寄存器
中
00BE1853 ?push ? ? ? ?eax ? ? ? ? ? ? ? ? ? ? ?//將eax的值壓棧,esp-4
00BE1854 ?mov ? ? ? ? ecx,dword ptr [ebp-8] ? ?//傳遞a,將ebp-8處放的3放在ecx寄存器中
00BE1857 ?push ? ? ? ?ecx ? ? ? ? ? ? ? ? ? ? ?//將ecx的值壓棧,esp-4
//跳轉調用函數
00BE1858 ?call ? ? ? ?00BE10B4 ?
00BE185D ?add ? ? ? ? esp,8 ?
00BE1860 ?mov ? ? ? ? dword ptr [ebp-20h],eax

函數調用過程
//跳轉調用函數
00BE1858 ?call ? ? ? ?00BE10B4 ?
00BE185D ?add ? ? ? ? esp,8 ?
00BE1860 ?mov ? ? ? ? dword ptr [ebp-20h],eax

????????call 指令是要執行函數調用邏輯的,在執行call指令之前先會把call指令的下一條指令的地址進行壓棧操作,這個操作是為了解決當函數調用結束后要回到call指令的下一條指令的地方,繼續往后執行。

當我們跳轉到Add函數,就要開始觀察Add函數的反匯編代碼了。

int Add(int x, int y)
{
00BE1760 ?push ? ? ? ?ebp ?//將main函數棧幀的ebp保存,esp-4
00BE1761 ?mov ? ? ? ? ebp,esp ? //將main函數的esp賦值給新的ebp,ebp現在是Add函數的ebp
00BE1763 ?sub ? ? ? ? esp,0CCh ?//給esp-0xCC,求出Add函數的esp
00BE1769 ?push ? ? ? ?ebx ? ? ? //將ebx的值壓棧,esp-4
00BE176A ?push ? ? ? ?esi ? ? ? //將esi的值壓棧,esp-4
00BE176B ?push ? ? ? ?edi ? ? ? //將edi的值壓棧,esp-4int z = 0; ? ? ?
00BE176C ?mov ? ? ? ? dword ptr [ebp-8],0 ?//將0放在ebp-8的地址處,其實就是創建zz = x + y;//接下來計算的是x+y,結果保存到z中
00BE1773 ?mov ? ? ? ? eax,dword ptr [ebp+8] ? //將ebp+8地址處的數字存儲到eax中
00BE1776 ?add ? ? ? ? eax,dword ptr [ebp+0Ch] ?//將ebp+12地址處的數字加到eax寄存中
00BE1779 ?mov ? ? ? ? dword ptr [ebp-8],eax ? ?//將eax的結果保存到ebp-8的地址處,其實就是放到z中return z;
00BE177C ?mov ? ? ? ? eax,dword ptr [ebp-8] ? ?//將ebp-8地址處的值放在eax中,其實就是把z的值存儲到eax寄存器中,這里是想通過eax寄存器帶回計算的結果,做函數的返回值。
}
00BE177F ?pop ? ? ? ? edi ?
00BE1780 ?pop ? ? ? ? esi ?
00BE1781 ?pop ? ? ? ? ebx ?
00BE1782 ?mov ? ? ? ? esp,ebp ?
00BE1784 ?pop ? ? ? ? ebp ?
00BE1785 ?ret ?

代碼執行到Add函數的時候,就要開始創建Add函數的棧幀空間了。

在Add函數中創建棧幀的方法和在main函數中是相似的,在棧幀空間的大小上略有差異而已。

  • 將main函數的 ebp 壓棧
  • 計算新的 ebp 和 esp
  • 將 ebx , esi , edi 寄存器的值保存
  • 計算求和,在計算求和的時候,我們是通過 ebp 中的地址進行偏移訪問到了函數調用前壓棧進去的參數,這就是形參訪問。
  • 將求出的和放在 eax 寄存器準備帶回

????????圖片中的 a' 和 b' 其實就是 Add 函數的形參 x , y 。這里的分析很好的說明了函數的傳參過程,以及函數在進行值傳遞調用的時候,形參其實是實參的一份拷貝。對形參的修改不會影響實參。

6. 函數棧幀的銷毀

當函數調用要結束返回的時候,前面創建的函數棧幀也開始銷毀。 那具體是怎么銷毀的呢?我們看一下反匯編代碼。

00BE177F ?pop ? ? ? ? edi ?//在棧頂彈出一個值,存放到edi中,esp+4
00BE1780 ?pop ? ? ? ? esi ?//在棧頂彈出一個值,存放到esi中,esp+4
00BE1781 ?pop ? ? ? ? ebx ?//在棧頂彈出一個值,存放到ebx中,esp+4
00BE1782 ?mov ? ? ? ? esp,ebp ?//再將Add函數的ebp的值賦值給esp,相當于回收了Add函數的棧幀空間
00BE1784 ?pop ? ? ? ? ebp ?//彈出棧頂的值存放到ebp,棧頂此時的值恰好就是main函數的ebp,esp+4,此時恢復了main函數的棧幀維護,esp指向main函數棧幀的棧頂,ebp指向了main函數棧幀的棧底。
00BE1785 ?ret ? ? ? ? ? ? ?//ret指令的執行,首先是從棧頂彈出一個值,此時棧頂的值就是call指令下一條指令的地址,此時esp+4,然后直接跳轉到call指令下一條指令的地址處,繼續往下執行。

回到了call指令的下一條指令的地方:

但調用完Add函數,回到main函數的時候,繼續往下執行,可以看到:

00BE185D ?add ? ? ? ? esp,8 ? ? ? ? ? ? ? ? ?//esp直接+8,相當于跳過了main函數中壓棧的 'a'和b'
00BE1860 ?mov ? ? ? ? dword ptr [ebp-20h],eax ?//將eax中值,存檔到ebp-0x20的地址處,其實就是存儲到main函數中ret變量中,而此時eax中就是Add函數中計算的x和y的和,可以看出來,本次函數的返回值是由eax寄存器帶回來的。程序是在函數調用返回之后,在eax中去讀取返回值的。
拓展了解

????????其實返回對象時內置類型時,一般都是通過寄存器來帶回返回值的,返回對象如果時較大的對象時,一般會在主調函數的棧幀中開辟一塊空間,然后把這塊空間的地址,隱式傳遞給被調函數,在被調函數中通過地址找到主調函數中預留的空間,將返回值直接保存到主調函數的。具體可以參考《程序員的自我修養》一書的第10章。 ?

????????到這里我們給大家完整的演示了main函數棧幀的創建,Add函數棧幀的創建和銷毀的過程,相信大家已經能夠基本理解函數的調用過程,函數傳參的方式,也能夠回答開始的問題了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/94768.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/94768.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/94768.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

廣告牌安全監測系統綜合解決方案

一、方案背景 廣告牌作為城市戶外廣告的重要載體&#xff0c;廣泛分布于城市道路、商業區及交通樞紐等人流密集區域。由于長期暴露在自然環境中&#xff0c;廣告牌面臨著風荷載、雨雪侵蝕、溫度變化等多重因素的影響&#xff0c;其結構安全性和穩定性直接關系到公共安全。近年來…

MII的原理

一、介紹 MII 是 Media Independent Interface&#xff08;媒體獨立接口&#xff09; 的縮寫&#xff0c;是一種用于連接網絡物理層&#xff08;PHY&#xff09;芯片和數據鏈路層&#xff08;MAC&#xff09;芯片的標準硬件接口&#xff0c;核心作用是讓不同類型的物理層&…

【Excel】Excel的工作場景

一、Excel的發展歷史 1.1 版本迭代周期 自Excel 2019版本起&#xff0c;微軟將更新周期穩定在每3年一次&#xff0c;而3年的周期剛好平衡了創新與穩定&#xff1a;既能緊跟大數據時代下用戶對自動化、智能化處理的需求&#xff08;比如近年數據量激增帶來的批量處理需求&#x…

nestjs 連接redis

1、下載npm install --save nestjs-modules/ioredis ioredis2、全局模塊中引用RedisModule.forRootAsync({useFactory: (configService: ConfigService) > {return {type:"single",url: configService.get(redis.url) };},inject: [ConfigService], }),整體如下&…

需求管理需要哪些角色配合

需求管理是項目管理的關鍵組成部分&#xff0c;它確保項目目標得到準確理解并能順利實現。有效的需求管理需要多個角色的緊密配合&#xff0c;包括項目經理、產品經理、需求分析師、開發人員、測試人員等。這些角色共同協作&#xff0c;確保需求從收集、分析、實施到驗證的每一…

SqlHelper類的方法詳細解讀和使用示例

在 C# 數據庫編程中&#xff0c;SqlHelper類是簡化 SQL Server 操作的重要工具&#xff0c;它封裝了ADO.NET的底層細節&#xff0c;讓開發者能更專注于業務邏輯。以下從方法原理、使用示例和實戰技巧三個方面進行詳細說明。 一、SqlHelper 核心方法原理與對比 SqlHelper的方法…

智能一卡通系統通過集成身份識別、權限管理、數據聯動等技術,實現多場景一體化管理。以下是多奧基于最新技術趨勢和應用案例的系統解析

智能一卡&#xff08;碼、臉&#xff09;通系統包括消費系統、梯控、門禁、停車場管理、訪客機等&#xff0c;需要了解這些系統的集成應用和最新技術發展。多奧打算從以下幾個維度來講解。智能一卡通系統的整體架構和主要功能模塊各子系統(門禁、梯控、停車場、訪客管理等)的技…

嵌入式學習日志————USART串口協議

1.通信接口通信的目的&#xff1a;將一個設備的數據傳送到另一個設備&#xff0c;擴展硬件系統通信協議&#xff1a;制定通信的規則&#xff0c;通信雙方按照協議規則進行數據收發名稱引腳雙工時鐘電平設備USARTTX&#xff08;數據發送腳&#xff09;、RX&#xff08;數據接收腳…

微軟硬件筆試面試核心題型詳細解析

微軟硬件筆試面試核心題型詳細解析 本專欄預計更新90期左右。當前第42期-筆試面試核心題型詳細解析. 本文一共4個章節,核心內容如下。 微軟作為全球頂尖的科技公司,其硬件工程師的選拔標準極高。筆試不僅考察扎實的理論基礎,更注重解決實際問題的能力、對新技術的理解以…

CMake構建學習筆記21-通用的CMake構建腳本

在之前使用CMake構建程序的腳本&#xff08;CMake構建學習筆記-目錄&#xff09;中&#xff0c;大部分內容都有比較強的相似性&#xff0c;那么是不是可以這些相似的內容提取出來作為一個單獨的腳本&#xff0c;在構建具體的程序的時候再去調用這個腳本呢&#xff1f;這樣做的好…

無人機和無人系統的計算機視覺-人工智能無人機

無人機和無人系統的計算機視覺-人工智能無人機將計算機視覺與無人系統相結合&#xff0c;可以提升其自主或半自主執行復雜任務的能力。這些系統將圖像數據與其他機載傳感器&#xff08;例如 GNSS/GPS、IMU、LiDAR 和熱像儀&#xff09;融合&#xff0c;以解讀周圍環境并執行精確…

【開題答辯全過程】以 基于hadoop架構的教學過程監控系統為例,包含答辯的問題和答案

個人簡介&#xff1a;一名14年經驗的資深畢設內行人&#xff0c;語言擅長Java、php、微信小程序、Python、Golang、安卓Android等開發項目包括大數據、深度學習、網站、小程序、安卓、算法。平常會做一些項目定制化開發、代碼講解、答辯教學、文檔編寫、也懂一些降重方面的技巧…

坎坷基金路

2025年8月27日上午10:59從基金委官網上中外合作入口查到自己的基金中了。心情頓時五味雜陳&#xff0c;回想起寫基金忙碌的9個月&#xff0c;各位專家對我的指導&#xff0c;嘴角楠楠的說&#xff1a;感恩。自己覺著比較重要的幾個點&#xff1a;1、立意必須基于自己的過往研究…

[n8n]

docs&#xff1a;n8n工作流管理系統 本項目幫助管理和探索n8n工作流。 它能自動掃描和分析工作流文件&#xff0c;提取關鍵信息如名稱、觸發器和關聯服務。 所有數據將存入可搜索的數據庫&#xff0c;并通過REST API提供訪問。 可以快速定位特定工作流、查看詳細描述&#xff0…

[手寫系列]Go手寫db — — 第二版

[手寫系列]Go手寫db — — 第二版 第一版文章&#xff1a;[手寫系列]Go手寫db — — 完整教程 整體項目Github地址&#xff1a;https://github.com/ziyifast/ZiyiDB請大家多多支持&#xff0c;也歡迎大家star??和共同維護這個項目~ 本文主要介紹如何在 ZiyiDB 第一版的基礎上…

私有化大模型基礎知識

私有化大模型基礎知識 文章目錄私有化大模型基礎知識0x01.開源閉源2. 數據成本&#xff1a;昂貴且隱形的開銷3. 研發投入&#xff1a;人力與時間成本總結&#xff1a;總成本量化更重要的是&#xff1a;持續投入和機會成本0x02.模型大小0x03.模型參數0x04.CPU和GPU0x05.GPU和模型…

Django時區處理

Django 的時區處理機制是為了確保在全球部署應用時&#xff0c;時間數據始終一致、可控&#xff0c;并能根據用戶或系統需求靈活轉換。下面我來系統地拆解一下 Django 的時區處理方式&#xff0c;幫你掌握從配置到實際應用的全過程。&#x1f9ed; 1. 基礎配置&#xff1a;USE_…

SqlHelper類庫的使用方法

使用 SqlHelper.dll 時&#xff0c;首先需要在項目中引用該 DLL&#xff0c;然后通過其封裝的方法簡化 SQL Server 數據庫操作。以下是常見操作的 C# 示例代碼&#xff1a;查看SqlHelper.dll方法內容// 替換為實際的SqlHelper.dll路徑 using System.Reflection; using Microsof…

蒼穹外賣項目實戰(日記十一)-記錄實戰教程及問題的解決方法-(day3-3)完善菜品分頁查詢功能

菜品分頁查詢 &#xff08;1&#xff09;需求分析 &#xff08;2&#xff09;代碼開發分析 DTO 前端給后端 &#xff0c;VO 后端給前端&#xff1b; vo是進行頁面展示&#xff0c;dto是前后端數據交互的&#xff0c;pojo是對應數據庫表字段 &#xff08;3&#xff09;DishCo…

C++ 力扣 704.二分查找 基礎二分查找 題解 每日一題

文章目錄二分查找&#xff1a;從基礎原理到代碼實現二分查找的特點&#xff1a;細節是坑&#xff0c;學會是寶算法重點&#xff1a;原理不只是“有序”&#xff0c;模板要懂不要背題目描述&#xff1a;LeetCode 704. 二分查找為什么這道題值得弄懂&#xff1f;為什么可以用二分…