ARM單片機啟動流程(三)(棧空間綜合理解及相關實際應用)

文章目錄

  • 1、引出棧空間問題
  • 2、解決問題
    • 2.1、RAM空間
    • 2.2、RAM空間具體分布
    • 2.3、關于棧空間的使用
    • 2.4、棧溢出
    • 2.5、變量的消亡
    • 2.6、回到關鍵字static
    • 2.7、合法性的判斷


1、引出棧空間問題

static關鍵字引出該部分內容。

為什么能從static引出來?

在使用該關鍵字的時候:

我們需要知道什么時候使用該關鍵字?

什么使用關鍵字?

并且我們知道函數執行的時候是在棧空間,但是我們的static修飾的關鍵字變量是在data段或者bss段。

還有就是我們的程序是從FLASH里面燒寫的,那就是意味著所有的變量以及函數都是先出現在FLASH里面也就是ROM空間。

以上這些疑問接下來通過按鍵開源項目例程主意分析。

2、解決問題

2.1、RAM空間

我們知道RAM里面有棧空間、堆空間、bss、data段。

  • ?棧空間(Stack)??:存儲函數調用的局部變量、參數、返回地址等,由系統自動管理,從高地址向下生長。

  • ?堆空間(Heap)??:用于動態內存分配(如 malloc),由程序員手動管理,從低地址向上生長。

  • ?.bss 段?:存儲未初始化的全局變量和靜態變量,程序啟動時由系統自動清零。

  • ?.data 段?:存儲已初始化的全局變量和靜態變量,程序啟動時從 Flash 復制初始值到 RAM。

但是需要聲明的是在裸機開發中一般不使用堆空間,

并且函數的執行都是在==棧空間==,那說到這里還記不記得有一個棧頂空間,對的,這個棧頂空間就是給一個上限,因此棧空間的特殊性,是從上到下的,也就是高字節到低字節分配,

  • 棧是一種線性數據結構,僅允許在棧頂(Top)?進行插入(入棧)和刪除(出棧)操作。類似一摞盤子,最后放上的盤子最先被取走。

這是因為在_main函數到mainARM內核還有一段代碼需要執行,因此留出來的是這一段空間,然后才是我們自己寫的main函數棧頂地址,就這后面的棧頂空間就可以循環利用了。

我們首先需要知道棧頂地址是怎么得到的?

2.2、RAM空間具體分布

這是整個RAM的空間:

![[Pasted image 20250711161229.png]]

棧是RAM頂部的最后一個區域,符合典型設計。這句話至關重要。

    Exec Addr    Load Addr    Size         Type   Attr      Idx    E Section Name        Object0x20000000   COMPRESSED   0x00000024   Data   RW           39    .data               main.o0x20000024   COMPRESSED   0x00000040   Data   RW          110    .data               modbus_app.o0x20000064   COMPRESSED   0x000000b5   Data   RW          183    .data               mb.o0x20000119   COMPRESSED   0x00000003   PAD0x2000011c   COMPRESSED   0x0000000c   Data   RW          267    .data               mbrtu.o0x20000128   COMPRESSED   0x00000008   Data   RW          372    .data               modbus_slave.o0x20000130   COMPRESSED   0x00000024   Data   RW          581    .data               key_drv.o0x20000154   COMPRESSED   0x00000024   Data   RW          618    .data               led_drv.o0x20000178   COMPRESSED   0x00000008   Data   RW          668    .data               ntc_drv.o0x20000180   COMPRESSED   0x00000006   Data   RW          810    .data               rh_drv.o0x20000186   COMPRESSED   0x00000002   PAD0x20000188   COMPRESSED   0x0000000c   Data   RW          964    .data               systick.o0x20000194   COMPRESSED   0x0000001c   Data   RW         1010    .data               usb2com_drv.o0x200001b0   COMPRESSED   0x00000002   Data   RW         1133    .data               portevent.o0x200001b2   COMPRESSED   0x00000002   PAD0x200001b4   COMPRESSED   0x00000018   Data   RW         1168    .data               portserial.o0x200001cc   COMPRESSED   0x00000004   Data   RW         3445    .data               mc_w.l(stderr.o)0x200001d0   COMPRESSED   0x00000004   Data   RW         3734    .data               mc_w.l(stdout.o)0x200001d4        -       0x00000100   Zero   RW          265    .bss                mbrtu.o0x200002d4   COMPRESSED   0x00000004   PAD0x200002d8        -       0x00000030   Zero   RW          580    .bss                key_drv.o0x20000308        -       0x00000014   Zero   RW          666    .bss                ntc_drv.o0x2000031c   COMPRESSED   0x00000004   PAD0x20000320        -       0x00000400   Zero   RW         3383    STACK               startup_gd32f30x_hd.o

通過工程的map文件可以看出在棧空間確定之前,首先確定的是data、bss數據占用的RAM空間,最后確定出棧空間的最低地址是多少。通過代碼可以看出是0x20000320,大小是0x00000400,其中棧的大小是可以自己設定的。那么兩者相加就是0x20000320 + 0x00000400 = 0x20000720

    pxMBFrameCBByteReceived                  0x2000007c   Data           4  mb.o(.data)pxMBFrameCBTransmitterEmpty              0x20000080   Data           4  mb.o(.data)pxMBPortCBTimerExpired                   0x20000084   Data           4  mb.o(.data)pxMBFrameCBReceiveFSMCur                 0x20000088   Data           4  mb.o(.data)pxMBFrameCBTransmitFSMCur                0x2000008c   Data           4  mb.o(.data)__stderr                                 0x200001cc   Data           4  stderr.o(.data)__stdout                                 0x200001d0   Data           4  stdout.o(.data)ucRTUBuf                                 0x200001d4   Data         256  mbrtu.o(.bss)__initial_sp                             0x20000720   Data           0  startup_gd32f30x_hd.o(STACK)

從最后一行代碼也可以看出該工程的棧頂地址是0x20000720

并且也符合圖片中的順序。

我們現在是在裸機層面考慮,所以先不考慮堆空間。

2.3、關于棧空間的使用

?ARM Cortex-M啟動流程與棧初始化?,在芯片上電或復位后,硬件自動執行以下步驟:

初始化主堆棧指針(MSP)??:從向量表的第一個表項(地址0x00000000或0x08000000)加載MSP初始值,指向棧頂(高地址)。也就是我們常說的這一步:

![[Pasted image 20250711202933.png]]

參考鏈接ARM單片機啟動流程(一)(詳細解析)-CSDN博客

讀取了棧頂地址以后,接著就是進入Rest_Handler復位函數地址,然后從這里開始執行程序,這里需要說明但是SP指向的地方。

首先,?棧頂(SP)已指向預設的棧空間頂端?(例如0x20000428),但尚未為任何函數分配棧幀。棧頂地址本身并不存儲_ _main函數的入口地址,而是由硬件直接設置SP寄存器的值。

接著需要引入一個:棧幀概念:
?棧幀的創建?:
當Reset_Handler調用__main時,會在棧上為__main創建棧幀,保存返回地址(LR)和寄存器上下文。 ?棧頂(SP)此時指向__main棧幀的頂部?(低地址)

需要注意的是__main的低地址也就是main函數的高地址。

也就是下述這個例子。

![[Pasted image 20250711202134.png]]

__main函數:

  • 將已初始化的全局變量(.data段)從Flash復制到RAM。 這個地方就解決了我們所疑惑的代碼燒寫到ROM里面,但是那些全局變量什么的又會到RAM里面。
  • 清零未初始化的全局變量(.bss段)。
  • 初始化C運行時環境(堆、棧、庫函數)。
  • 最終調用用戶main()函數。

用戶main()及其調用的子函數共享同一棧空間,通過SP的移動動態分配/釋放棧幀,實現內存高效利用。

也就是說在整個RAM空間(不考慮堆空間),能循環利用的地方也就是棧空間,更具體來講就是main下面的。

因為在最下面是data、bss段,往上就是堆空間,接著就是我們的棧空間了。而棧空間又分為最上面的棧頂空間是用來存__main這個棧幀空間的,接下來就是main以及可以循環利用的棧空間,全靠SP移動高效復用內存。

?特性?通用系統(如 Linux)嵌入式系統(無 OS)
?**main() 行為**?單次執行后退出無限循環,永不退出
?**__main 棧幀生命周期**?main() 返回后立即釋放永久保留(因 main() 不退出)
?棧溢出風險?遞歸過深導致循環內局部變量過大或遞歸未限制
?退出處理?調用 atexit()、析構全局對象進入 halt 或復位
  • __main 棧幀是“永久居民”?**?:因 main() 永不返回,它作為程序生命周期的基石始終存在棧底。

  • ?子棧幀是“流動工人”??:在 main() 的循環中動態輪轉,通過 SP 移動高效復用內存。

  • ?循環缺失 = 系統失控?:嵌入式環境中,main() 退出即程序終結,棧幀管理失去意義。

2.4、棧溢出

在裸機嵌入式系統中,棧溢出可能覆蓋bss段和data段,尤其是當棧與靜態存儲區相鄰且無保護時。

先不考慮堆空間。

  • 棧(Stack)??:從高地址向低地址增長(向下增長)。

  • ?堆(Heap)??:從低地址向高地址增長(向上增長)。

  • ?靜態存儲區?:

    • ?data段?:存放已初始化的全局變量和靜態變量。
    • ?bss段?:存放未初始化的全局變量和靜態變量(程序啟動時清零)。
  • bss段優先被覆蓋?:

    • bss段通常緊鄰堆區,位于棧的下方(低地址方向)。
    • 若棧溢出量較大,?最先覆蓋的是bss段?(因其位置更靠近棧底)。
    • ?案例?:
      在Jflash下載算法中,棧溢出導致.bss段變量被覆蓋,引發Flash寫入錯誤(如數據被篡改)。
  • ?data段可能被覆蓋?:

    • 若bss段被完全覆蓋且溢出持續,棧會進一步向下覆蓋data段。
    • data段存儲已初始化變量,覆蓋可能導致程序邏輯錯誤或數據損壞?(如配置參數丟失)。

若內存布局中堆區較大或存在保護間隙(Guard Region)?,棧溢出可能僅覆蓋堆區,未觸及bss/data段。

某些鏈接腳本(Linker Script)會隔離棧與其他段,例如在棧底預留保護區。

通常bss段最先被覆蓋,其次是data段(因位置更接近棧底)

并且可以通過鏈接腳本隔離、哨兵檢測、MPU保護或靜態分析,可有效預防覆蓋風險。

?哨兵檢測(Sentinel Detection)?? 是一種通過監控特定內存值來識別棧溢出的軟件方法。其核心原理是在棧空間邊界預設一個特殊標記值(哨兵值),通過定期檢查該值是否被篡改來判斷是否發生溢出。

設置哨兵值?
在棧空間的頂部或底部?(根據棧增長方向)預留一個位置,寫入特定的哨兵值(如 0xDEADBEEF)。棧通常從高地址向低地址增長(如ARM Cortex-M),哨兵值需放置在棧頂(低地址邊界)。

#define STACK_SENTINEL_VALUE 0xDEADBEEF
volatile uint32_t stack_sentinel __attribute__((section(".stack"))) = STACK_SENTINEL_VALUE;

定期檢查哨兵值?
在系統空閑任務、定時器中斷或關鍵任務周期中調用檢測函數,驗證哨兵值是否被覆蓋:

void check_stack_overflow(void) {if (stack_sentinel != STACK_SENTINEL_VALUE) {// 棧溢出處理handle_overflow_error();}
}

并且哨兵檢測具有滯后性、漏檢風險等局限性

  • 只能在溢出發生后檢測,無法預防溢出,結合棧著色(Stack Coloring)技術,填充全棧空間并計算高水位線,提前預警。
  • 若溢出未覆蓋哨兵值(如局部變量過大但未觸及邊界),可能漏檢。可在函數入口處增加棧指針范圍檢查。

2.5、變量的消亡

  • ?靜態存儲區?:
    • ?data段?:存放已初始化的全局變量和靜態變量。
    • ?bss段?:存放未初始化的全局變量和靜態變量(程序啟動時清零)。
      需要說明的是:bss和data不會釋放的,會一直占用。

局部變量在函數棧幀(Stack Frame)?? 中分配空間。當函數被調用時,編譯器會移動棧指針(如 sub rsp, N 指令),為所有局部變量一次性分配內存。

函數返回時,通過指令(如 add rsp, Nmov rsp, rbp)將棧指針移回函數調用前的位置,?整個棧幀的內存被標記為“可復用”?,局部變量的存儲空間隨之釋放

  • 釋放操作是高效的指針移動,而非數據擦除(內存中可能殘留原值)。
  • 若后續函數調用覆蓋該棧幀,殘留數據會被新數據替換。

局部變量的生命周期與其所屬函數的棧空間緊密綁定,其存儲空間確實隨著函數棧幀的銷毀而被釋放。

棧空間釋放后,局部變量的地址立即失效,但數據可能暫時殘留。訪問這些地址會導致未定義行為?(如野指針操作)。
這里也就解釋了前面學習指針內容中,為什么我們要對指針指向明確的地址,就是防止野指針發生,因為有時候可能恰好就會指向我們剛好釋放的棧幀空間,那不就導致數據錯誤了。 產生程序崩潰(段錯誤)、數據污染(覆蓋其他變量)。

是不是這里又豁然開朗了,簡直是太妙了!!!!!!!!!!

2.6、回到關鍵字static

使用了static就說明這個變量不會隨著函數棧幀的內存被標記為“可復用”而消失。

我們使用static關鍵字主要有兩個方面
1、控制作用域和封裝

  • 限制作用域?:static 將變量作用域限定在當前文件內,其他文件無法通過 extern 訪問這些變量。這避免了全局變量的“污染”,防止其他模塊意外修改按鍵狀態。
  • ?封裝性?:按鍵操作邏輯(如掃描、消抖)通常集中在同一文件中。static 變量使所有相關操作內聚,符合“高內聚、低耦合”的設計原則。
    2、?模塊化設計與協作開發
  • 避免命名沖突?:全局變量可能被多人協作時的其他文件同名變量覆蓋,而 static 變量僅在當前文件有效,徹底消除沖突風險。
  • ?簡化調試與維護?:開發者只需關注當前文件內的邏輯,無需追蹤全局變量的跨文件調用鏈,降低認知負擔。
    3、內存與生命周期管理
  • 生命周期相同,但更安全?:static 變量與全局變量均存儲在靜態數據區,生命周期均為整個程序運行期。但 static 通過作用域限制,提供了自動的內存隔離,避免全局變量的無約束訪問。
  • ?初始化保障?:static 變量默認初始化為 0(如未顯式初始化),與全局變量一致,但僅在首次加載時初始化一次。
?特性?static Button btn1, btn2;全局變量 Button btn1, btn2;
?作用域?僅當前文件整個程序(所有文件)
?跨文件訪問?不可訪問可通過 extern 訪問
?命名沖突風險?幾乎為零高(需靠命名約定管理)
?內存位置?靜態數據區(與全局變量相同)靜態數據區
?初始化?默認 0,僅初始化一次默認 0,程序啟動時初始化
?適用場景?模塊內共享數據,無需外部暴露需跨模塊共享的全局數據

因此在這里我們使用static關鍵字。

2.7、合法性的判斷

編程思想的嚴謹性在這里需要體現。

即使 static 變量地址有效,若函數通過參數接收外部指針(如 button_init(&btn1, ...)),仍需檢查該參數是否為空:

因此初始化的時候首先要進行檢測的就是判斷地址的合法性。

void button_init(Button* handle, ...) {if (!handle) return;  // 必須檢查,避免外部誤傳 NULL
}

文章源碼獲取方式:
如果您對本文的源碼感興趣,歡迎在評論區留下您的郵箱地址。我會在空閑時間整理相關代碼,并通過郵件發送給您。由于個人時間有限,發送可能會有一定延遲,請您耐心等待。同時,建議您在評論時注明具體的需求或問題,以便我更好地為您提供針對性的幫助。

【版權聲明】
本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議。這意味著您可以自由地共享(復制、分發)和改編(修改、轉換)本文內容,但必須遵守以下條件:
署名:您必須注明原作者(即本文博主)的姓名,并提供指向原文的鏈接。
相同方式共享:如果您基于本文創作了新的內容,必須使用相同的 CC 4.0 BY-SA 協議進行發布。

感謝您的理解與支持!如果您有任何疑問或需要進一步協助,請隨時在評論區留言。

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

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

相關文章

【RK3568+PG2L50H開發板實驗例程】FPGA部分 | 鍵控LED實驗

本原創文章由深圳市小眼睛科技有限公司創作,版權歸本公司所有,如需轉載,需授權并注明出處(www.meyesemi.com) 1.實驗簡介 實驗目的: 從創建工程到編寫代碼,完成引腳約束,最后生成 bit 流下載到…

【Python練習】039. 編寫一個函數,反轉一個單鏈表

039. 編寫一個函數,反轉一個單鏈表 039. 編寫一個函數,反轉一個單鏈表方法 1:迭代實現運行結果代碼解釋方法 2:遞歸實現運行結果代碼解釋選擇方法迭代法與遞歸法的區別039. 編寫一個函數,反轉一個單鏈表 在 Python 中,可以通過迭代或遞歸的方式反轉一個單鏈表。 方法 1…

BERT代碼簡單筆記

參考視頻:BERT代碼(源碼)從零解讀【Pytorch-手把手教你從零實現一個BERT源碼模型】_嗶哩嗶哩_bilibili 一、BertTokenizer BertTokenizer 是基于 WordPiece 算法的 BERT 分詞器,繼承自 PreTrainedTokenizer。 繼承的PretrainedTokenizer,具…

PID控制算法理論學習基礎——單級PID控制

這是一篇我在學習PID控制算法的過程中的學習記錄。在一開始學習PID的時候,我也看了市面上許多的資料,好的資料固然有,但是更多的是不知所云。(有的是寫的太過深奧,有的則是照搬挪用,對原理則一問三不知&…

【Elasticsearch】function_score與rescore

它們倆都是用來“**干涉評分**”的,但**工作階段不同、性能開銷不同、能做的事也不同**。一句話總結:> **function_score** 在 **第一次算分** 時就動手腳; > **rescore** 在 **拿到 Top-N 結果后** 再“重新打分”。下面把“能干嘛”…

無廣告純凈體驗 WPS2016 精簡版:移除聯網模塊 + 非核心組件,古董電腦也能跑

各位辦公小能手們!今天給你們介紹一款超神的辦公軟件——WPS2016精簡版!它有多小呢?才33MB,簡直就是軟件界的小不點兒!別看它個頭小,功能可一點兒都不含糊,文字、表格、演示這三大功能它全都有。…

《PyWin32:Python與Windows的橋梁,解鎖系統自動化新姿勢》

什么是 PyWin32在 Windows 平臺的 Python 開發領域中,PyWin32 是一個舉足輕重的庫,它為 Python 開發者打開了一扇直接通往 Windows 操作系統底層功能的大門。簡單來說,PyWin32 是用于 Python 訪問 Windows API(Application Progra…

vite如何生成gzip,并在服務器上如何設置開啟

1. 安裝插件npm install vite-plugin-compression -D2. 在 vite.config.ts 中配置TypeScriptimport { defineConfig } from vite import compression from vite-plugin-compressionexport default defineConfig({plugins: [compression({algorithm: gzip,ext: .gz,threshold: 1…

1068萬預算!中國足協大模型項目招標,用AI技術驅動足球革命

中國足協啟動國際足聯“前進計劃”下的大數據模型項目,預算1068萬元。該項目將建立足球大數據分析平臺,利用AI技術為國家隊、青少年足球、業余球員及教練員裁判員提供精準數據分析服務,旨在通過科技手段提升中國足球競技水平。 中國足球迎來數…

AI產品經理面試寶典第12天:AI產品經理的思維與轉型路徑面試題與答法

多樣化思維:如何跳出單一框架解題? 面試官:AI產品常面臨復雜場景,請舉例說明你如何運用多樣化思維解決問題? 你的回答:我會從三個維度展開:多角度拆解需求本質,多層級融合思維模式,多變量尋找最優解。比如設計兒童教育機器人時,不僅考慮功能實現(技術層),還融入情…

vscode.window對象講解

一、vscode.window 核心架構圖 #mermaid-svg-fyCxPz1vVhkf96nE {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-fyCxPz1vVhkf96nE .error-icon{fill:#552222;}#mermaid-svg-fyCxPz1vVhkf96nE .error-text{fill:#5522…

為什么一個 @Transactional 注解就能開啟事務?揭秘 Spring AOP 的底層魔法

你是否也曾深陷在各種“額外”邏輯的泥潭,為了給一個核心業務方法增加日志、權限校驗或緩存,而不得不將這些非核心代碼硬塞進業務類中,導致代碼臃腫、職責不清?是時候用代理設計模式 (Proxy Design Pattern) 來解脫了!…

《Spring 中上下文傳遞的那些事兒》Part 8:構建統一上下文框架設計與實現(實戰篇)

📝 Part 8:構建統一上下文框架設計與實現(實戰篇) 在實際項目中,我們往往需要處理多種上下文來源,例如: Web 請求上下文(RequestContextHolder)日志追蹤上下文&#xf…

配置驅動開發:初探零代碼構建嵌入式軟件配置工具

前言在嵌入式軟件開發中,硬件初始化與寄存器配置長期依賴人工編寫重復代碼。以STM32外設初始化為例,開發者需手動完成時鐘使能、引腳模式設置、參數配置等步驟,不僅耗時易錯(如位掩碼寫反、模式枚舉值混淆)&#xff0c…

Elasticsearch混合搜索深度解析(下):執行機制與完整流程

引言 在上篇中,我們發現了KNN結果通過SubSearch機制被保留的關鍵事實。本篇將繼續深入分析混合搜索的執行機制,揭示完整的處理流程,并解答之前的所有疑惑。 深入源碼分析 1. SubSearch的執行機制 1.1 KnnScoreDocQueryBuilder的實現 KNN結果被…

Apache HTTP Server 從安裝到配置

一、Apache 是什么?Apache(全稱 Apache HTTP Server)是當前最流行的開源Web服務器軟件之一,由Apache軟件基金會維護。它以穩定性高、模塊化設計和靈活的配置著稱,支持Linux、Windows等多平臺,是搭建個人博客…

php中調用對象的方法可以使用array($object, ‘methodName‘)?

是的,在PHP中,array($object, methodName) 是一種標準的回調語法,用于表示“調用某個對象的特定方法”。這種語法可以被許多函數(如 call_user_func()、call_user_func_array()、usort() 等)識別并執行。 語法原理 在P…

【設計模式】單例模式 餓漢式單例與懶漢式單例

單例模式(Singleton Pattern)詳解一、單例模式簡介 單例模式(Singleton Pattern) 是一種 創建型設計模式,它確保一個類只有一個實例,并提供一個全局訪問點來獲取這個實例。(對象創建型模式&…

vue3 el-table 行數據沾滿格自動換行

在使用 Vue 3 結合 Element Plus 的 <el-table> 組件時&#xff0c;如果你希望當表格中的行數據文本過長時能夠自動換行&#xff0c;而不是溢出到其他單元格或簡單地截斷&#xff0c;你可以通過以下幾種方式來實現&#xff1a;方法 1&#xff1a;使用 CSS最簡單的方法是通…

windows電腦遠程win系統服務器上的wsl2

情況 我自己使用win11筆記本電腦&#xff0c;想要遠程win11服務器上的wsl2 我這里只有服務器安裝了wsl2&#xff0c;win11筆記本沒有安裝 因此下面提到的Ubuntu終端指的是win服務器上的wsl2終端 一定要區分是在哪里輸入命令&#xff01;&#xff01; 安裝SSH 在服務器上&#x…