電子產業(yè)一站式賦能平臺

PCB聯(lián)盟網(wǎng)

搜索
查看: 119|回復: 0
收起左側

例說堆棧模型(加量增強版)

[復制鏈接]

491

主題

491

帖子

3109

積分

四級會員

Rank: 4

積分
3109
跳轉到指定樓層
樓主
發(fā)表于 2024-9-19 11:39:00 | 只看該作者 |只看大圖 回帖獎勵 |正序瀏覽 |閱讀模式

【說在前面的話】
朋友:
你知道如何設置棧最安全么?你知道如何不寫一行匯編代碼就能設置棧的大小么?
你知道如何在鏈接腳本中使用宏和頭文件么?
你知道如何在代碼中隨時隨地檢查棧的最大使用情況么?
本文從理論到實踐,從知其然到知其所以然,一杯奶茶的功夫就給你講得明明白白。



在中文嵌入式環(huán)境中,時不時的總能看到不少朋友”堆”“棧“傻傻分不清楚,我很早之前在文章《漫談C變量——夏蟲不可語冰》介紹過二者的區(qū)別,這里就不再深入展開,總之:
棧(Stack)“是我們用來分配局部變量、實現(xiàn)函數(shù)調用和在異常響應時保存被打斷代碼上下文的地方——具體細節(jié)不重要,在本文的討論中,我們只需要記住以下信息:
  • Cortex-M系統(tǒng)棧的生長方向是自上而下的,也就是隨著更多內容被壓入(PUSH)棧中,棧頂指針的地址值是越來越小的——也就是從地址值較大的位置向地址值較小的位置移動。Cortex-M的棧頂指針指向的是“棧頂部的空位”。
  • 從最大兼容性角度考慮,Cortex-M架構下棧存儲空間必須對齊到8字節(jié)。

    “堆(Heap)”是我們使用 malloc 申請動態(tài)存儲空間時所必須用到的一種數(shù)據(jù)結構——通常由C語言的系統(tǒng)庫提供。
  • 堆本身只是一個內存管理的算法,它所要管理的RAM空間需要用戶通過某種手段將指定大小的RAM空間交到Heap算法手里
  • 與棧不同,堆的生長方向其實完全由具體的管理算法決定,而堆的算法數(shù)量雖然不能說是燦若星辰,至少一雙手肯定數(shù)不過來——但一般來說我們可以大體認為堆的生長方向是“自下而上的”——也就是從地址值較小的位置延伸到地址值較大的位置。
  • 堆的對齊要求一般是4字節(jié)起步,8字節(jié)更好,情況不明的直接就32個字節(jié)吧。


    【常見的堆棧模型】
    從單純從我不負責任的經驗來看,由很多GCC領銜使用的“對向生長”模型可能是嵌入式領域最常見的”大聰明模型“,沒有之一。如下圖所示:

    先說優(yōu)點吧:
    該模型棧和堆共用同一塊連續(xù)的地址區(qū)間配置時不需要操心具體棧有多大、堆有多大
  • 配置方法簡單:只需要指定這一整塊”堆!皡^(qū)域的起始地址,以及這一整塊堆棧區(qū)域的大小
  • 堆和棧的最大可用大小是此消彼長的,理論上可以在某種最優(yōu)的情況下達到動態(tài)的”此消彼長“,可以獲得理想狀下最大的空間復用效率。
    缺點也很明顯:
  • 堆和棧的最大可用大小是此消彼長的,在真實場景中,由于”你長我也長誰怕誰”的情況居多,發(fā)生隨機性的“雙向奔赴”從而進行“負距離”的互動可能性從理論上就不可避免,因而是系統(tǒng)穩(wěn)定性的“一生之敵”。
  • 實驗室里7x24小時完美通過,一去客戶那里就隨機性宕機的“挖坑之王”


    為了提高系統(tǒng)穩(wěn)定性,人們簡單地將“堆”和“!辈痖_來單獨配置,就獲得了常見的“兩段式堆棧模型”:

    可以看到,相較之前的模型,雖然仍然是“對向生長”,但由于棧和堆有了自己固定空間,因此可以方便地根據(jù)實際用量調整它們的大。ū热缌粝伦銐虻挠嗔浚,從而降低彼此入侵帶來的穩(wěn)定性風險。更有甚者,在二者的邊界上引入一個特殊值(比如0xDEADBEEF)所充當?shù)囊绯鰴z測”金絲雀(Canary)”——一旦發(fā)現(xiàn)這個值與預設的不同,基本就可以斷定發(fā)生了溢出。

    【最安全的“兩面包夾芝士”模型】
    將“棧(Stack)”和"堆(Heap)"獨立配置的“兩段式”模型配合邊界金絲雀,為預防和檢測堆棧溢出提供了可能。但對金絲雀的檢測總歸有種“事后諸葛亮”的感覺,而且很多時候,我們是想不起來去檢查金絲雀的,比如:棧曾經一度跨越雷池入侵到了堆空間,但由于此時堆恰巧分配出去的RAM不多,沒有與棧發(fā)生實質性的重疊,因而整個系統(tǒng)“安然無恙”——這只能說是運氣好,而風險肯定是存在的——正由于系統(tǒng)“安然無恙”,因此我們在系統(tǒng)開發(fā)階段可能不會想起來去檢查一下金絲雀(有自動檢查機制的RTOS除外),那么這類溢出就有可能被隱藏。
    基于上述原因,有沒有一種方法可以:
  • 徹底避免棧/堆入侵對系統(tǒng)的破壞
  • 在棧/堆入侵的瞬間就立即表現(xiàn)出來——方便我們在調試階段立即發(fā)現(xiàn)
    答案是肯定的,這就是“兩面包夾芝士”模型(此前又叫“三明治”模型):

    從上圖很容易看出:
    該模型屬于“兩段式模型”的變種
  • 與過去堆和棧的“相向生長”不同,該模型采用了“背向生長”的方式——避免了棧與堆的相互傷害
  • 棧被放在了SRAM的起始位置(Cortex-M從架構上鼓勵將SRAM放置在從0x2000-0000開始的地址上),這樣一旦發(fā)生棧溢出,指針就會指向SRAM存儲器以外的無效位置——這在大部分芯片上會觸發(fā)“Bus Fault”,從而產生故障異常——這就實現(xiàn)了對棧溢出的當場捕獲,并且不依賴MPU或者“棧底地址限制檢測(Stack Limit Checking)”之類的架構特性。當然有些芯片設計者可能會選擇“隱藏這類錯誤”,不僅不會觸發(fā)異常,而且會當做無事發(fā)生,具體表現(xiàn)為:對無效地址的寫入操作將被無視,對無效地址的讀取操作將會返回0值。具體可以參考芯片手冊,或者干脆做個實驗。
  • 堆被放置在了RAM的最后,中間夾著存放靜態(tài)/全局變量的“RW/ZI區(qū)域”,這也是“兩面包夾芝士”模型(或者“三明治”模型)名稱的由來。這樣的安排也徹底杜絕了棧和堆對“RW/ZI區(qū)域”發(fā)生入侵的可能。當堆溢出時,與棧類似,對大部分芯片來說都會觸發(fā)故障異常,從而在開發(fā)調試階段第一時間被我們所捕獲。


  • 通過鏈接腳本(比如Arm Compiler的Scatter Script或者gcc、clang的ld)的一些運算功能,我們甚至可以做到“將剩下的空間全留給HEAP”,從而簡化系統(tǒng)的配置。



    【Arm官方低調推薦的”新“方法】
    其實,Arm Compiler 在很久之前就逐步淘汰了“大聰明的單段對向生長模型”,而“兩段模型”早已成為主流。比如,我們在匯編啟動文件中經?梢砸姷竭@樣的代碼片段:


    這就是“兩段式”模型的證據(jù)。實際上,在啟動代碼的尾部,匯編程序通過:
  • IMPORT __use_two_region_memory選擇了對兩段式模型提供支持的libc庫:

    看過我前面一期文章《【嵌入式秘術】Cortex-M靜態(tài)鏈接庫——從入坑到入土》的小伙伴一定會眼前一亮——“原來是這樣啊,我們其實是手動選擇了對應兩段式堆棧模型的庫版本呢”。

    問題是,我們要如何在Arm Compiler環(huán)境下實現(xiàn)“兩面包夾芝士”模型呢?我們需要寫匯編代碼么?
    不用擔心,即便你的啟動文件是匯編的,具體操作方法也非常簡單。步驟如下:
    步驟一:準備階段
    注意:此步驟只針對使用匯編啟動文件的情況。如果你的啟動文件是C,則可跳過該步驟。
    在工程管理器中找到你的匯編啟動文件,它通常以
  • startup_.s的形式命名:


    找到配置棧和堆大小的部分(紅框標注的部分):

    將其整體刪除(或者注釋掉)。注意:請保留這里的 PRESERVE8和THUMB部分。
    繼續(xù)移動到匯編文件的尾部,找到如下的代碼:



    同理,將其刪除(或者注釋掉)。
    注意:這里要保留 END 。
    移動到中斷向量表的定義處:


    將紅框中所標注的代碼選中:
  • __Vectors       DCD      __initial_sp替換為如下內容:
  •                 IMPORT   |Image$$ARM_LIB_STACK$$ZI$$Limit|
    __Vectors       DCD      |Image$$ARM_LIB_STACK$$ZI$$Limit|即:

    保存啟動文件。
    此時,如果你著急編譯,當你當你開啟了microLib時,很可能會看到如下的鏈接錯誤:

    即:
  • Error: L6218E: Undefined symbol __initial_sp (referred from entry2.o).
    或者你沒有開啟 microLib,則會看到一個不同的錯誤:

    即:
  • Error: L6915E: Library reports error: The semihosting __user_initial_stackheap cannot reliably set up a usable heap region if scatter loading is in use這都是正常的,不必驚慌。這類錯誤會在完成后面的步驟后自然消失。
    步驟二:獲取鏈接腳本(Scatter Script)
    打開工程配置窗口“Options for Target”,切換到“Linker”選項卡:


    首先,一定要確保你勾選了圖中的“Use Memory layout from Target Dialog”選項。在這一前提下,再次取消對它的勾選:

    我們會看到,MDK基于當前的Memory Layout,為我們在Out目錄下生成了一個與工程同名的鏈接腳本(比如圖中的工程名叫example,因此生成的鏈接腳本為 example.sct)。單擊 Edit 按鈕,可以看到腳本的內容:


    先別著急半路開香檳——該文件是系統(tǒng)自動生成的,如果我們不移動它的位置,那么只要哪次手抖勾選了“Use Memory Layout from Target Dialog”,它的內容就會立即被覆蓋掉——意味著我們在后續(xù)步驟中所做的修改就會付諸東流。

    為了避免該問題,應該將它從 Out 目錄中移動到工程目錄下。具體步驟為,右鍵單擊腳本文件名:


    選擇“Open Container Folder”來打開文件所在目錄:

    找到Scatter Script腳本文件后,將其拷貝到上一級目錄下(也就是工程目錄):

    重新打開工程配置窗口:

    確保我們“沒有”選中“Use Memory Layout from Target Dialog”選項,并在Scatter File文本框中直接填寫我們剛剛拷貝出來的腳本文件名(由于我們直接放在工程目錄下,因此這里直接用相對路徑"./example.scat"或者"example.scat"就行)。單擊OK保存配置。
    步驟三:在鏈接腳本中部署堆和棧在編輯器中打開我們的腳本文件:


    圖中選中的部分實際上包含了RAM中的所有內容,包括靜態(tài)變量、全局變量、棧和堆:

    是的,你的猜測沒錯:當我們沒有特別說明時,Stack和Heap都以ZI的形式存在于上述空間內,其位置任由Linker擺布——這當然也帶來了很多不確定性。
    接下來我們要做的就是按照我們的設計——“兩面包夾芝士”來明確的指定棧和隊列的大小和位置:


    我們要做的是首先將一個名為ARM_LIB_STACK 的execution region放置到RAM的起始位置:
  •   ARM_LIB_STACK 0x20000000 ALIGN 8 EMPTY 0x800 {}這里:
  • 起始地址是 0x20000000
  • STACK的大小是 0x800
  • ALIGN 8 指定對齊是8個字節(jié)
  • EMPTY是必須要保留的,它用來說明 ARM_LIB_STACK 是一個大數(shù)組,里面默認填充了0。
  • 如果你想修改填充的內容還可以通過關鍵字 FILL 來指定填充的32bit數(shù)值,比如:
  • ARM_LIB_STACK 0x20000000 ALIGN 8 FILL 0xDEADBEEF EMPTY 0x800 {}它實現(xiàn)了往0x20000000開始的0x800(2KB)大小的?臻g中填充0xDEADBEEF的功能:

    熟悉“水印法”測量棧用量的小伙伴一定大喜。

    為了讓ZI/RW緊隨其后——放在STACK的后面,我們需要對 RW_IRAM1 的描述進行修改,即從:
  • RW_IRAM1 0x20000000 0x00020000  {修改為:
  • RW_IRAM1 +0  {即:

    這里,我們在原本放置地址0x20000000的位置用"+0"表示“緊隨其后”,并刪除了原本的大小0x00020000——這樣做就是告訴編譯器“RW_IRAM1”不限制大小。
    接下來,我們要用類似的方法緊隨 RW_IRAM1 之后放置名為 ARM_LIB_HEAP 的execution region——用來指定堆的位置和大。
  • ARM_LIB_HEAP +0 ALIGN 8 EMPTY 0x200 {}可以看到,這里與棧的設置方式幾乎一樣,而“+0”則同樣告訴linker:請將ARM_LIB_HEAP緊鄰前面的 RW_IRAM1 放置。最終的效果如下:
  • LR_IROM1 0x00000000 0x00040000  {      ER_IROM1 0x00000000 0x00040000  {     *.o (RESET, +First)   *(InRoot$$Sections)   .ANY (+RO)   .ANY (+XO)  }
      ARM_LIB_STACK 0x20000000 ALIGN 8 EMPTY 0x800 {}
      ;RW_IRAM1 0x20000000 0x00020000  {  ; RW data  RW_IRAM1 +0  {  ; RW data   .ANY (+RW +ZI)  }
      ARM_LIB_HEAP +0 ALIGN 8 EMPTY 0x200 {}}

    還記得我們前面刪除了原本對RW_IRAM1的尺寸限制(也就是0x0002000)么?這意味著,現(xiàn)階段的腳本文件對我們實際使用的RAM空間是沒有任何限制的——換句話說,如果超出了芯片實際的SRAM大小,編譯器也是不會報告錯誤的。為了重新加入這一限制,我們可以在 ARM_LIB_HEAP的后面加入下面的語句:

  • ScatterAssert(ImageLimit(ARM_LIB_HEAP) 這里:
  • ScatterAssert() 是讓linker對括號中的內容進行檢查
  • ImageLimit() 是在編譯時刻獲得括號內指定 execution region 的終止地址
  • 0x20000000+0x20000 是例子中整個RAM的終止地址(這里假設RAM從0x20000000開始,大小是0x20000
  • 綜合來說,上述代碼的作用是在linker的鏈接階段計算HEAP的終止地址,確認它是否落在了RAM的有效范圍內。
    如果超出了范圍,我們就會看到如下的編譯錯誤:
  • Error: L6388E: ScatterAssert expression (ImageLimit(ARM_LIB_HEAP) 最終效果如下:

    對應的“兩面包夾芝士”圖示如下:


    編譯工程:




    【“雖遲但到”的宏和頭文件】
    是的,你猜得沒錯,我們可以在鏈接腳本中使用編譯預處理,這意味著:
  • 我們可以使用宏
  • 我們可以include頭文件
  • 我們可以進行條件編譯
    具體方法并不難,只需要在鏈接腳本的“第一行”,注意一定要是第一行(Number One)——前面不能有任何內容,空行或者注釋都不行——放置如下的內容:
  • #! armclang --target=arm-arm-none-eabi -mcpu=cortex-m0 -E -xc

    然后我們就可以在腳本文件中愉快地使用宏和include了?吹侥_本中這么多的常數(shù)了么?地址啊、大小啊,這下都可以用宏替代了。比如:
  • #define RAM1_SIZE    0x00020000#define RAM1_BASE    0x20000000#define RAM1_LIMIT   (RAM1_BASE + RAM1_SIZE)
    #define STACK_SIZE   0x800#define HEAP_SIZE    0x200

    其實我們還可以把宏的定義部分放置到專門的配置頭文件中——通過#include來包含——從而真正做到一個配置頭文件定天下。至于宏可以有哪些騷操作,感興趣的小伙伴可以關注【裸機思維】公眾號后,發(fā)送關鍵字“”來獲取相關文章,這里就不再贅述。
    需要注意的是:
  • 在較新版本的MDK中,上述方法“應該”同時支持Arm Compiler 5(armcc)Arm Compiler 6(armclang)。你可以關注【裸機思維】公眾號后,發(fā)送關鍵字“MDK”來獲取最新的MDK。
    對于某些較老的MDK來說,如果你使用的是 Arm Compiler 5,則需要把添加在 scatter script 第一行的命令行修改為:
  • #! armcc --cpu Cortex-M0 -E...以解決可能出現(xiàn)的編譯錯誤。
  • 如果你的頭文件并沒有“直接”放置在工程目錄下,而是存在一個相對路徑,則可以通過在上述命令行中追加 -I 的形式來告知編譯器去哪里搜索我們的頭文件。比如:
  • #! armclang --target=arm-arm-none-eabi -mcpu=cortex-m0 -E -xc -I ../../cfg或者
  • #! armcc --cpu Cortex-M0 -E -I ../../cfg則是告訴編譯器從相對路徑 "../../cfg" 下去搜索頭文件。


    當你通過修改頭文件的方式來更新scatter script的內容后,第一次編譯,請務必一定要以“Rebuild All”的形式進行,否則你的修改不會生效。

    別說我沒提醒過你哦!

    【如何把剩余的空間都留給堆】
    很多時候,把剩余空間都留給堆是一個不錯的想法,這樣“兩面包夾芝士”模型就獲得了和“單段相向生長”模型一樣的優(yōu)勢——配置簡單。由于我們已經有了宏的幫助,借助 ImageLimit() 我們可以將 HEAP_SIZE 的宏定義修改為:
  • #define HEAP_SIZE    (RAM1_LIMIT - ImageLimit(RW_IRAM1))它的意思是:用RAM1的終止地址減去 RW_IRAM1的終止地址,獲得中間的差額,其圖示如下:


    看似完美,有的小伙伴一編譯就會報告如下的錯誤:


    即:
  • Error: L6388E: ScatterAssert expression (ImageLimit(ARM_LIB_HEAP) = (0x20000000 + 0x20000)) failed on line 29 : (0x20020004 0x20020000)奇怪,我們的計算公式應該沒錯啊——Heap的尺寸應該就是使用整個 RAM的終止地址減去 RW_IRAM1 的終止地址啊,為什么提示差4個字節(jié)呢?
    聰明的小伙伴一定已經注意到了,我們在 ARM_LIB_HEAP 的定義中,指定了其首地址的對齊為8字節(jié):
  • ARM_LIB_HEAP +0 ALIGN 8 EMPTY HEAP_SIZE {}而 RW_IRAM1 的尺寸不一定是8的整倍數(shù),當它只是“4的整倍數(shù)”而不滿足“8的整倍數(shù)”這一條件時,ImageLimit(RW_IRAM1) 的后面與 ARM_LIB_HEAP的起始地址之間就會產生一個4字節(jié)的氣泡


    要解決這一問題也很簡單,我們可以使用 scatter script 腳本為我們提供的一個專門來進行地址對齊的函數(shù):
  • AlignExpr(地址數(shù)值>,對齊要求>)比如:
  • AlignExpr(ImageLimit(RW_IRAM1), 8)就表示對 RW_IRAM1 的終止地址進行 8 字節(jié)對齊。借助它的幫助,我們可以修改腳本如下:
  • #define HEAP_SIZE    \    (RAM1_LIMIT - AlignExpr(ImageLimit(RW_IRAM1), 8))即:

    再編譯時,已然沒有問題。



    【如何隨時隨地的了解棧的最大使用情況】
    水印法是實現(xiàn)“最大棧用量統(tǒng)計”的最有效方式。其原理也不復雜:
  • 先用指定的水印常數(shù)(比如 0xDEADBEEF)將整個棧填滿;
  • 從?臻g的最初頂部(棧存儲空間的終止地址)向下開始搜索之前填充的水印常數(shù)——一旦碰到水印,就將當前已經經歷過的RAM總量作為棧的最大深度(最大用量);[/ol]

    對于步驟1來說,可以通過前面介紹的 FILL 關鍵字來完成對?臻g的填充:
  • ARM_LIB_STACK 0x20000000 ALIGN 8 FILL 0xDEADBEEF EMPTY STACK_SIZE{}然后借助下面的代碼完成統(tǒng)計工作:
  • #if defined(__clang__)#   pragma clang diagnostic push#   pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"#   pragma clang diagnostic ignored "-Wdouble-promotion"#endifuint32_t calculate_stack_usage_topdown(void){    extern uint32_t Image$$ARM_LIB_STACK$$Limit[];    extern uint32_t Image$$ARM_LIB_STACK$$Length;
        uint32_t *pwStack = Image$$ARM_LIB_STACK$$Limit;    uint32_t wStackSize = (uintptr_t)&Image$$ARM_LIB_STACK$$Length / 4;    uint32_t wStackUsed = 0;
        do {        if (*--pwStack == 0xDEADBEEF) {            break;        }        wStackUsed++;    } while(--wStackSize);            printf("\r
    Stack Usage: [%d/%d] %2.2f%%\r
    ",             wStackUsed * 4,             (uintptr_t)&Image$$ARM_LIB_STACK$$Length,            (   (float)wStackUsed * 400.0f             /   (float)(uintptr_t)&Image$$ARM_LIB_STACK$$Length));
        return wStackUsed * 4;}#if defined(__clang__)#   pragma clang diagnostic pop#endif這里有幾點需要說明一下:
  • armlink 為我們提供了通用的語法來獲取 execution region 的起始地址、大小和終止地址:
  • extern uint32_t Image$$$$Base[];extern uint32_t Image$$$$Length;extern uint32_t Image$$$$Limit[];這里,BaseLimit被定義成了不定長數(shù)組的形式,因此我們可以直接把它們當做常量指針來使用——獲取所需的地址。Length被定義成了一個普通的uint32_t型的變量,按照官方文檔的要求,雖然很反直覺,但如果要獲取它的值——也就是對應execution region的大小,必須要對其進行&操作,并隨后強制轉化為整形數(shù)值。這么說也許有點抽象,不妨對照前面的代碼來看:
  • #include ...extern uint32_t Image$$ARM_LIB_STACK$$Limit[];extern uint32_t Image$$ARM_LIB_STACK$$Length;
    uint32_t *pwStack = Image$$ARM_LIB_STACK$$Limit;uint32_t wStackSize = (uintptr_t)&Image$$ARM_LIB_STACK$$Length / 4;這里,我們通過 Image$ARM_LIB_STACK$$Limit[] 將棧的終止地址賦值給了(uint32_t *)型的指針 pwStack。以表達式 (uintptr_t)&Image$$ARM_LIB_STACK$$Length 獲取了 ARM_LIB_STACK 的實際大小。
  • 普通情況下,在變量名中使用 “$” 會在Arm Compiler 6引發(fā)警告:
  • warning: '$' in identifier [-Wdollar-in-identifier-extension]為了讓編譯器閉嘴,我們臨時對函數(shù) calculate_stack_usage_topdown() 在編譯時刻做了屏蔽warning的操作:
  • #if defined(__clang__)#   pragma clang diagnostic push#   pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"#   pragma clang diagnostic ignored "-Wdouble-promotion"#endifuint32_t calculate_stack_usage_topdown(void){    ...}#if defined(__clang__)#   pragma clang diagnostic pop#endif而 -Wdouble-promotion 則是由printf中的百分比運算引起的,一并屏蔽即可。
    在任意時刻,當我們想要知道當前系統(tǒng)的最大棧用量時,可以直接調用函數(shù) calculate_stack_usage_topdown(),比如:
  • int main(void){    ...    calculate_stack_usage_topdown();    ...}一個可能的執(zhí)行結果如下:


    自上而下統(tǒng)計棧用量的方法優(yōu)點是:當?臻g很大而實際棧用量較小時,可以較快的完成統(tǒng)計;缺點是:如果恰好棧里因為任何原因(比如用戶定義了一個局部變量,然后恰好給他賦予了我們的水印常數(shù)),就會造成統(tǒng)計錯誤——沒能實際獲得最大深度。
    針對這一問題,我們可以修改搜索策略,從占空間的起始地址(也就是基地址)處向上搜索“非水印常數(shù)”——一旦碰到,就可以用已知的?臻g尺寸減去已經經歷過的RAM總量作為棧的最大深度(最大用量)。

    該方法的優(yōu)點是:不容易發(fā)生誤判;缺點是:當?臻g很大而實際棧用量較小時往往較為耗時。對應的代碼如下:
  • #if defined(__clang__)#   pragma clang diagnostic push#   pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"#   pragma clang diagnostic ignored "-Wdouble-promotion"#endifuint32_t calculate_stack_usage_bottomup(void){    extern uint32_t Image$$ARM_LIB_STACK$$Base[];    extern uint32_t Image$$ARM_LIB_STACK$$Length;
        uint32_t *pwStack = Image$$ARM_LIB_STACK$$Base;    uint32_t wStackSize = (uintptr_t)&Image$$ARM_LIB_STACK$$Length;    uint32_t wStackUsed = wStackSize / 4;
        do {        if (*pwStack++ != 0xDEADBEEF) {            break;        }    } while(--wStackUsed);        printf("\r
    Stack Usage: [%d/%d] %2.2f%%\r
    ",             wStackUsed * 4,             wStackSize,            ((float)wStackUsed * 400.0f / (float)wStackSize));
        return wStackUsed * 4;}#if defined(__clang__)#   pragma clang diagnostic pop#endif




    【后記】
    在這篇文章中,我們介紹了棧和堆在存儲器中的常見排布模型,比較了它們的優(yōu)劣,并提出了一種被稱為“兩面包夾芝士”的兩段式模型。該模型:可以有效避免堆棧溢出破壞常規(guī)變量溢出發(fā)生時可以在大部分芯片中第一時間觸發(fā)異!晃覀儾蹲降
    后面,我們以MDK為例介紹了如何在Arm Compiler環(huán)境下應用這一模型,并引入了使用宏對其進行進一步拓展的方法。

    值得說明的是,這一方法對Arm Compiler 5(armcc)Arm Compiler 6(armclang)同樣適用。支持MicroLib和非MicroLib的情況。無論啟動文件是否為匯編,都可以正常工作。

    實際上,使用鏈接腳本而非匯編啟動文件來對兩段式堆棧模型進行配置是Arm公司一直以來所提倡的。隨著Arm Compiler 6的逐步普及,更多的芯片公司正在追隨Arm的腳步將原本的匯編啟動文件替換為 CMSIS 目錄下所提倡的純C語言啟動文件。
    作為【反復橫跳】系列的一部分,我希望通過這篇文章能幫助大家掃清從Arm Compiler 5Arm Compiler 6過渡圖中與棧相關的障礙。希望對你有所幫助。
    猜你喜歡:
    WiFi6+藍牙+星閃,三合一開發(fā)板,真香!
    Github上熱門 C 語言項目匯總!
    嵌入式,可測試性軟件設計!
    一些低功耗軟件設計的要點!
    嵌入式 C 保護結構體的方式
    實用 | 10分鐘教你通過網(wǎng)頁點燈
    談談嵌入式軟件的兼容性!
    分享一個嵌入式代碼生成器設計思路!
    點擊閱讀原文,查看更多分享。
  • 回復

    使用道具 舉報

    發(fā)表回復

    您需要登錄后才可以回帖 登錄 | 立即注冊

    本版積分規(guī)則

    關閉

    站長推薦上一條 /1 下一條


    聯(lián)系客服 關注微信 下載APP 返回頂部 返回列表