STM32編程:動畫深度演示棧機制、棧溢出

[導讀] 從這篇文章開始,將會不定期更新關於嵌入式C語言編程相關的個人認爲比較重要的知識點,或者踩過的坑。

爲什麼要深入理解棧?做C語言開發如果棧設置不合理或者使用不對,棧就會溢出,溢出就會遇到無法預測亂飛現象。所以對棧的深入理解是非常重要的。

啥是棧

棧是一種受限的數據結構模型,其數據總是只能在頂部追加,利用一個指針進行索引,頂端叫棧頂,相對的一端底部稱爲棧底。棧是一種LIFO後入先出的數據結構。

棧就兩種操作:

  • PUSH,壓棧,向棧內加入數據,
  • POP,出棧

再進一步探討:

首先將棧與堆分清,從看到這篇文章開始,我建議你不要把堆和棧連在一起叫,棧是棧,堆是堆,這是兩回事,別混爲一談!(堆本文不深入討論)

從C/C++編程語言的角度來看:

  • 相同點:都是一片內存區,在鏈接時指定棧區/堆區的位置以及大小。

  • 不同點:

    • 棧:由編譯器分配,存放函數的參數值,局部變量,寄存器組(不同的單片機/處理器各有不同)、函數調用參數傳遞、中斷異常產生時須保存處理器狀態的寄存器值等
    • 堆:由程序員分配釋放,對於C而言,malloc、realloc/free進行分配/釋放,對C++而言,由new/delete分配/釋放。

爲啥用

棧這個數據模型的應用價值是什麼呢?先來看一下單片機內部的可能有哪些棧應用?以STM32爲例,參考IAR C/C++ Development
Guide,P207

處理器模式 建議段名 描述
Supervisor SVC_STACK 操作系統棧
IRQ IRQ_STACK 通用(IRQ)中斷處理程序的堆棧。
FIQ FIQ_STACK 用於高速(FIQ)中斷處理程序的堆棧。
Undefined UND_STACK 堆棧用於未定義的指令中斷。 支持硬件協處理器和指令集擴展的軟件仿真。
Abort ABT_STACK 用於指令獲取和數據訪問存儲器中止中斷處理程序的堆棧。

如果使用RTOS還有任務棧,如果是Linux,其內核線程同樣也需要棧的支持,等等這一切的一切棧,其本質上都是利用了棧數據模型的LIFO後入先出的特性,一個典型應用場景就是比如做一件事情做到一半而要轉而去做另外一件事,對於芯片編程而言,就需要將當前的工作做個暫存,等另外一件事情做完了,再接着回來繼續做。那麼怎麼做到呢,以一箇中斷處理爲例,要記住當前的工作態有哪些信息需要暫存呢?PC指針,局部變量等就被壓入棧,再將中斷服務程序地址導入PC指針,進而去執行中斷服務程序,待中斷處理完畢,在將棧裏的內容按照後入先出彈出到對應的寄存器就恢復了原程序的現場,進而繼續執行。

怎麼用

棧在哪裏定義大小,定多大合適?這可能很多剛接觸單片機開發的同學不是太清楚,下面就將比較常見的IAR開發環境爲例如何定義棧定義棧大小的地方說明一下,這裏以IAR8.4.1爲例,有兩種方式可以進行棧大小設置。

IDE設置

爲了更加清楚明瞭,製作了一個GIF操作展示視頻,在stack/heap中就可以設置了,其中stack用於設置棧區大小,heap用於設置堆大小。
在這裏插入圖片描述

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-R0PYTPgM-1588081894306)(E:\blog\embInn\STM32\層層展開STM32棧的工作機制\pic\setStackIarIde.gif)]

這個demo中設置了其棧的大小爲0x200,堆的大小爲0x400,全編譯後,檢查map文件就印證了棧/堆的大小如預期所修改。

鏈接配置文件

其實對於比較熟悉的開發人員,上一種方式並非推薦用法。用鏈接配置文件將具有更好的靈活性,比如可以指定一個段的對齊方式,存儲位置,某個符號的存儲位置等等。這裏同樣爲了直觀也做了一個GIF動畫,介紹如何通過鏈接文件進行棧/堆的大小配置。
在這裏插入圖片描述
其最終的效果也一樣如預期將棧區的大小設置好了。

棧溢出

這裏爲了比較容易的展示棧溢出的問題,在main函數利用遞歸方法計算階乘,代碼如下:

#include <stdio.h>
#include "main.h"
static uint32_t spSatte[200];
static uint32_t spIndex = 0;
/*爲什麼要用浮點數,因爲數據非常大整型很快就會溢出*/
float factorial(uint32_t n)
{
    uint32_t sp = __get_MSP();    
    /*記錄棧指針的變化情況*/
    spSatte[spIndex++] = sp;
    if(n==0 || n==1)
        return 1;
    else
        return (float)n*factorial(n-1);
}

int main(void)
{
    float  x = 0;
    uint32_t  n = 20;
    printf("stack test:\n");
    x = factorial(n);
    /*打印棧指針變化情況*/
    for(int i = 0;i<spIndex;i++)
        printf("MSP->%08X\n",spSatte[i]);
    
    /*打印階乘結果*/
    printf("factorial(%d)=%f\n",n,x);    
    while (1)
    {
    }
}

爲方便觀察,將stm32f407xx_flash.icf 將棧改爲256字節

/*stm32f407xx_flash.icf 將棧改爲256字節*/
define symbol __ICFEDIT_size_cstack__ = 0x200;
define symbol __ICFEDIT_size_heap__   = 0x200;

全編譯後通過map文件來看下棧/堆的分配情況:

"P2", part 3 of 3:                          0x400
  CSTACK                      0x2000'05d8   0x200  <Block>
    CSTACK           uninit   0x2000'05d8   0x200  <Block tail>
  HEAP                        0x2000'07d8   0x200  <Block>
    HEAP             uninit   0x2000'07d8   0x200  <Block tail>
                            - 0x2000'09d8   0x400

直觀些,翻譯成下圖,CSTACK段分配在0x2000 05D8-0x2000 07D8,堆分配在0x2000 0480-0x2000 0680。

在這裏插入圖片描述
圖爲什麼沒有將0x2000 07D8畫在棧區呢?通過調試發現,這個字空間沒有用做棧的實際存儲。將工程設置成simulation模式,debug進入main.o勾選掉,我們來計算20的階乘,來具體看一下:
在這裏插入圖片描述
對這個動圖解讀一下:

  • 進入復位是,SP_main爲0x200007D8,指向棧底,爲空棧。那麼這是怎麼實現的呢?
__vector_table                ;向量表
        DCD     sfe(CSTACK)   ;這條命令會將程序的CSTACK起始地址裝載給SP_main
        DCD     Reset_Handler ; Reset Handler復位向量
  • 前面說0x200007D8並沒有用到,怎麼證明呢,在函數進入mian時,第一次壓棧的情況如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-w2nOc290-1588081894324)(E:\blog\embInn\STM32\層層展開STM32棧的工作機制\pic\image-20200419165114071.png)]

  • 可見STM32棧的增長方向是向下增長的,也即頂在小地址端一側
  • 棧存儲元素是四字節對齊的,因爲STM32的字長是字節,如果深入想想,如果不是司字節對齊會怎麼樣?留給感興趣的思考一下。
  • 0x200007D8–0x200007DB 這個字存儲單元並不是棧的有效存儲空間。

棧的變化情況:

stack test:
MSP->200007A8 
MSP->20000790
MSP->20000778
MSP->20000760
MSP->20000748
MSP->20000730
MSP->20000718
MSP->20000700
MSP->200006E8
MSP->200006D0
MSP->200006B8
MSP->200006A0
MSP->20000688
MSP->20000670
MSP->20000658
MSP->20000640
MSP->20000628
MSP->20000610
MSP->200005F8
MSP->200005E0
factorial(20)=2432902023163674771.785700 /*結算結果與用計算器一致*/

每調用一次階乘函數,棧就壓入4個字,由上面還可以看到第20次進入時,棧指針爲0x200005E0,如果再壓入4個字棧指針會變成0x200005C8,是這樣嗎,結果還對嗎?將n改爲21編譯運行,來看一看:
在這裏插入圖片描述

看到了吧,驚喜來了,棧溢出了,程序已經不聽話了,完全不知道在幹嘛了。所以棧溢出的後果是極端危險的,完全無法預期,程序會帶來什麼後果。

總結一下

  • 棧是一種LIFO後入先出的數據結構模型,是C/C++程序運行時基礎,沒這個棧,C/C++玩不轉
  • 棧在嵌入式編程領域隨處可見,比如C棧,中斷棧、異常棧、任務棧等等,但其基本工作原理都一樣。支持兩種基本數據操作:壓棧、出棧。
  • 棧溢出程序的結果無法預期,所以合理的設置棧區大小是個永恆的話題,過大則浪費內存,過小則程序會飛。
  • 嵌入式編程遞歸函數要慎用,個人建議不用。比如IEC61508 功能安全標準中強行規定不可使用遞歸函數。
  • STM32中__get_MSP可以得到當前棧指針的值,據此可以做一定程度的棧溢出保護措施。防止程序跑飛。
  • 通過上面遞歸調用測試,還可以得到一個啓示,嵌入式編程函數嵌套的層級不宜過深,過深則需要相對較大的棧開銷。

  • 版權聲明:所有文章版權歸嵌入式客棧所有,如商業使用,須嵌入式客棧授權。歡迎關注微信公衆號,內容更豐富。
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章