嵌入式開發如何統計運行占據內存

嵌入式linux 發佈 2022-12-04T23:31:07.797575+00:00

B:認為:全局所需內存=全局變量+ 局部變量+malloc,map只能統計靜態部分,不能統計動態部分,因為map是編譯靜態產生的,動態內存分為棧和堆,棧體現在動態變化的,map文件中,局部變量是看不到,即便是偏移地址,而在彙編中是可以看到的,C:認為:局部變量是靜態內存,編譯時確定,map裡面局部變量的地址是相對於函數的偏移。

1、各抒己見

  • 小明說:想要計算 一段算法在所占用的內存
  • A(筆者):
    • 建議看Map文件,map文件可以看到data 段 的一些占用size,以armcc 為例,以.o為單位,統一一個.o文件中的data段的size。
    • 所以我建議他放在一個文件,可以看到這個算法中.o文件的data段的大小,即就是全局變量以及靜態變量所占用的size
    • 如果有malloc的話,會另算。
    • 棧空間這塊的,我沒有考慮,棧是循環利用的,不是光算法占用,但是實際也應該考慮,如果棧消耗太大,則也會存在問題。
    • 聽同學說,如果代碼需要放在內存中執行,那麼這部分Code也需要占內存
  • B:認為:
    • 全局所需內存=全局變量(靜態內存部分)+ 局部變量(動態棧內存部分)+malloc(動態堆內存部分),
    • map只能統計靜態部分,不能統計動態部分,因為map是編譯靜態產生的,
    • 動態內存分為,棧體現在動態變化的,
    • map文件中,局部變量是看不到,即便是偏移地址,而在彙編中是可以看到的,(棧中的偏移地址)
  • C:認為:
    • 局部變量是靜態內存,編譯時確定,map裡面局部變量的地址是相對於函數的偏移。
    • 函數大小包括局部變量大小
    • 動態內存只有堆,沒有棧,如果局部變量很大,則會看到函數的體積變大
    • 編譯出可執行程序後,棧空間就不會增大了。
    • 遞歸多次,只會增大函數的體積,不會棧超,棧超了連結器會報錯
    • map文件可以看出棧小,導致棧溢出的問題。

2、筆者分析

筆者來說說看法,經過試驗得出的結果,以ARMCC、IAR以及GCC為例

2.1 ARMCC 分析

以一個例程來分析,led.c 最簡單的

u32 LEDValue1 = 0XFFFF;
const u32 LEDValue2=0XFFFF;
u32 LEDValue3[4];

void LED_Init(void)
{      
  GPIO_InitTypeDef  GPIO_InitStructure;

  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);//使能GPIOF時鐘
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_CRC, ENABLE);
 
  //GPIOF9,F10初始化設置
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12 |GPIO_Pin_13| GPIO_Pin_14;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通輸出模式
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推輓輸出
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;//上拉
  GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化
 
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
  GPIO_Init(GPIOD, &GPIO_InitStructure);//初始化

  GPIO_Write(GPIOD,LEDValue1);  //以下代碼都是為了測試
  GPIO_Write(GPIOE,LEDValue2);
  LEDValue3[0]=0XFFFF;
  GPIO_Write(GPIOE,LEDValue3[0]);
}
void LedRun()
{
  GPIO_ResetBits(GPIOD,GPIO_Pin_5);
  GPIO_SetBits(GPIOD,GPIO_Pin_5);
}
1234567891011121314151617181920212223242526272829303132

2.1.1 筆者A觀點

然後打開生成的map文件,可以看到具體編譯好的信息,很方便分析單個.o文件所占用的size信息,比如該文件led.c

從上面Map信息裡面看到Led.o的size情況:

  • Code:156 Byte
  • RW Data:4Byte
  • ZI Data:16Byte
  • RO Data:0Byte

所以如果算法單獨使用了一個.o文件,在armcc下,很容易分析出數據的空間使用大小。但是棧的空間+堆的空間沒有統計到,

堆是運行態的,靜態編譯出來的無法統計到,需要具體的情況具體分析,單獨去看malloc這種,或者自己內存管理的空間申請。

至於棧的使用空間,編譯階段可能不知道,因為編譯階段不知道調用關係,而連結的時候則由連結器將多個.o文件組織起來,所以可以知道調用關係,

  • 一個連結選項:–callgraph 就可以生成調用關係,
  • 同時會分析出使用棧的情況。從下圖可以看到 main->LED_Init->GPIO_Init ,LED_Init 初始化使用棧24Byte,

接著來分析一下,為啥LED_Init函數的棧使用了24Byte空間。

大家都知道,數據的運算以及函數的調用,都會用到寄存器,而用寄存器之前需要保存寄存器,所以棧主要是用來保存該函數用到的寄存器,來看一下彙編,很容易就明白了。

push的時候,都是4位元組對齊的(寄存器都是32位的),所以總共push了6個寄存器,總共24Byte。

push {r2-r6,r14} 
1

從上文可以看到LED.o共使用了156Byte

從map文件中來看,兩個函數分別是116Byte + 24Byte,共140Byte,由上文可知,共156Byte,那麼16Byte就是上文中的inc.data,就是Code中用到了一些數據,這些數據無法直接訪問,需要開闢一塊單元來存儲這些數據地址,然後才可以加載。如下面第二張圖所示,比如0x40021000,很明顯這個就不是Code的地址或者RAM的地址,就是一個外設地址(GPIOE),根據STM32的手冊可以得知(下面圖三)。

在這裡插入圖片描述

在這裡插入圖片描述

在這裡插入圖片描述

上文中RW Data 或者ZI Data 符合預期,但是RO Data 很奇怪是0,因為我們本身定義了一個const的類型的數據,但是統計竟然是0,

u32 LEDValue1 = 0XFFFF;
const u32 LEDValue2=0XFFFF;
u32 LEDValue3[4];
123

這個需要從彙編入手,編譯器也不傻,定義一個const 類型的數據,編譯器會就生成一個RO data嗎,不一定,比如本文這個,編譯器直接將0xFFFF 編譯到指令中,而不是從變量中加載數據,這個需要從彙編中看。

如何才能產生一個RO data呢?如果引用到變量的地址,那麼肯定會產生一個RO data,因為需要分配變量地址。例如下文中這樣。

u32 * data_p = (u32*)&LEDValue2;
*data_p = *data_p +
1;
GPIO_Write(GPIOE,LEDValue2);
123

然後分析map文件,可以看到RO data Size 為4,led.o中有了RO的變量以及地址,也可以看到ro data的地址不是在sram,而是在flash中(ROM)中,最後彙編也為const 變量申請了存儲空間(LEDValue2)

嵌入式物聯網需要學的東西真的非常多,千萬不要學錯了路線和內容,導致工資要不上去!

無償分享大家一個資料包,差不多150多G。裡面學習內容、面經、項目都比較新也比較全!某魚上買估計至少要好幾十。

點擊這裡找小助理0元領取:加微信領取資料





2.1.2 B同學觀點

對於B同學的觀點,我基本 都是贊同的。補充一下:就是所需要的內存,可能還需要加上Code所需要的空間(如果有這種場景的話,在內存中允許代碼)

對於棧是動態的理解,我的想法也是棧是動態變化的

  • 函數調用完成之後,棧就釋放了,還可以重新使用
  • 和堆相似,但是和堆不同的是,棧動態變化過程是相對固定的,就是編譯器編譯好指令之後,每個函數的棧使用Size就確定了,不會在變化了。
  • 唯一變化的可能就是一級一級的調用棧,這個連結器統計的有些情況可能不准,(統計最大size)
    • 比如出現環形調用,統計出來的情況就不准,類似遞歸調用,準是有出口的,但是編譯器不知道,就會統計出錯。
    • 還比如出現函數指針調用,編譯器可能也無法統計出最大的調用棧size,無法統計出具體的調用關係。

map文件是看不到局部變量的,原因有兩點,

  • 棧是動態變化的,會覆蓋掉,
  • 而且如果多個函數調用,調用路徑不一樣,那麼在棧中的偏移地址也不固定,所以說看不到的,
  • 即便是彙編中,可以看到的是部分變量壓棧,其他的可能還是在寄存器中使用,所以基本上地址無法確定。

map文件中看到的 全局變量 或者局部靜態變量。

2.1.2 C同學觀點

對於C同學的觀點,很多我都有不同的意見,

  • 對於第一條,map文件可以看到局部變量地址,這個我可以肯定是看不到的,除非進入函數那一刻,去獲取地址,但是靜態的map文件分析是看不到的。而且變量和代碼是分開存放的,及時能看到,也不在同一個區域,怎麼可能是函數地址的偏移呢???
  • 對於第二條,函數的大小,我認為不包括局部變量的大小,局部變量的使用在棧中(寄存器),而棧的使用體現在sp的變化,也就是指令上面,從map文件中也可以看到LED_Run這個函數的大小是24Byte,在彙編中統計一下指令的大小(左邊圈住的),恰好也是24Byte。

上面那個LedRun函數可能沒有局部變量,那我們來加一個局部變量來看看,例如下面的代碼,如果包括局部變量,那麼函數的size一定會超過100,畢竟還有指令的size,實際編出來 的map文件分析,看到函數大小為64,分析彙編代碼,指令數也是64,可以得出結論,函數的大小是不包括局部變量的。

void LedRun()
{
  u16 LEDData[100]={123};
  u8 i=0;
  for(i=0;i<100;i++)
    GPIO_Write(GPIOE,LEDData[i]); //測試,可能沒意義
  GPIO_ResetBits(GPIOD,GPIO_Pin_5);
  GPIO_SetBits(GPIOD,GPIO_Pin_5);
}
123456789

o文件的大小 包括了 code、data(RO RW ZI)的大小,也沒包括局部變量的大小。

但是函數本身使用的局部變量空間是可以統計出來的,剛剛也看到了,通過連結器生成的信息。

216 = (1002)+ Push(44 寄存器r4 r5 r6 r14)

  • 對於第三條,如第二條所述
  • 第四條,程序編譯好,棧空間的情況就不會變化了,這個也不是一定,比如有那種bank機制(下次介紹),由於Flash空間的限制,一些不常用的程序存放在nand裡面或者其他spi nor flash裡面,等用到的時候再加載,這樣棧的空間使用也會相對的動態增加,當然這屬於一種特殊情況。
  • 第五條,棧超了會報連結錯誤,這個不會的,連結器存在環這種情況的時候,統計出來的棧使用是不準的,所以沒法報錯誤,如果棧溢出了,可能會將其他空間踩了,引入其他bug。

舉例說明,本程序的棧空間是0x400,還是剛剛的程序,同樣可以編譯過,沒有報任何錯誤,還統計出來棧的使用情況(2064> 0x400(1024))。

void LedRun()
{
u16 LEDData[0x400]={123};
u16 i=0;
for(i=0;i<0x400;i++)
GPIO_Write(GPIOE,LEDData[i]);
//測試,可能沒意義
GPIO_ResetBits(GPIOD,GPIO_Pin_5);
GPIO_SetBits(GPIOD,GPIO_Pin_5);
}
123456789

在這裡插入圖片描述

  • 第六條,map文件會分析棧的情況,好像也沒有,至少對於armcc 編譯器來說,沒有統計棧的使用情況,而是在一個連結選項中 會專門生成棧的調用關係,以及所使用的棧情況

以上就是筆者分析的一些情況,有不同分意見可以分享評論。後面簡單以IAR以及arm-gcc 分析,看看是否有所不同。

附錄:

文章連結:https://mp.weixin.qq.com/s/KHwQDhKXXzNuX1agS27VjQ

轉載自:李肖遙 技術讓夢想更偉大

文章來源:嵌入式開發如何統計運行占據內存

版權聲明:本文來源網絡,免費傳達知識,版權歸原作者所有。如涉及作品版權問題,請聯繫我進行刪除。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

關鍵字: