文章目錄
- 2025年4月10日新增
- 分析PC寄存器指針值排查問題
- map文件設計到的知識點
- 1. **.bss 段(Block Started by Symbol)**
- 2. **.data 段**
- 3. **.text 段**
- 4. **.heap 段**
- 5. **.stack 段**
- 6. **.rodata 段(只讀數據段)**
- 7. **.init 和 .fini 段**
- 8. **符號信息(符號表)**
- 總結
- 如題
- 如何解決?
- 涉及到的知識點
- 記錄遇到的坑
2025年4月10日新增
首先程序框架已經解決所有問題,可以完美運行了!今天從git上download下來昨天的出錯版本,重新分析下昨天的錯誤程序
堆棧設置的是1K空間
這個uart_init()函數里面的buff數組太大導致了堆棧溢出,昨天出了好幾種堆棧溢出的情況,這是其一,那個systick初始化的問題我今天沒復現,忘記昨天啥情況了,上面即使uart_inti()函數不調用,但是只要上面定義了還是會棧溢出,為什么呢?
左邊error的RW多了0.25k空間,多出來的主要是ZI Data數據
發現error的map文件中,出現了stdio_streams.o,這個跟printf()函數有關
可以看出跟uart_init()函數沒有關系
注釋了這行代碼就解決問題了
分析兩個程序的內存布局差異及跑飛原因
一、內存布局差異對比
??堆棧空間分配??
??異常程序??:
STACK 段起始地址 0x20000500,大小 1024 bytes
HEAP 段起始地址 0x20000300,大小 512 bytes
??正常程序??:
STACK 段起始地址 0x200003f8,大小 1024 bytes
HEAP 段起始地址 0x200001f8,大小 512 bytes
??差異??:正常程序的堆棧和堆地址更緊湊,可能是通過調整啟動文件中的 Stack_Size 和 Heap_Size 宏定義優化了內存分布。
??全局變量區域(.bss/.data)??
??異常程序??的 .bss 段存在多模塊重疊分配(如 hal_uart1.o 和 stdio_streams.o),導致 active_config 等變量地址沖突(如 0x20000190 和 0x20000078)。
??正常程序??的 .bss 段地址分配連續且無重疊,例如 hal_uart1.o 的 rx_buffer 從 0x2000007c 開始,與前后模塊無沖突。
二、跑飛原因推測
??堆棧溢出??
??異常程序??的堆棧起始地址 0x20000500 與 .bss 段末端 0x200002a0 間隔僅 0x260 bytes(約 608 字節),若存在深層次函數調用或中斷嵌套,可能超過棧容量導致數據覆蓋。
??正常程序??的堆棧起始地址 0x200003f8 與 .bss 末端 0x20000198 間隔 0x260 bytes,但通過優化內存分布避免了關鍵變量被覆蓋。
??內存訪問越界??
??異常程序??中 hal_uart1.o 的 tx_buffer 分配了 132 bytes,但其后的 active_config 地址 0x20000190 可能被越界寫入(如環形緩沖區未正確管理),破壞相鄰數據。
??中斷嵌套未考慮??
若異常程序中存在高頻中斷(如 UART 接收中斷),且未預留足夠中斷棧空間(STM32 默認使用主堆棧 MSP),嵌套中斷可能耗盡 1024 bytes 的堆棧容量。
分析PC寄存器指針值排查問題
PC起始地址是0x8000240,是SystemInit
單步執行后發現PC指針跑飛
實際上,我這里是重定向出了問題
//int fputc(int ch, FILE *f)
//{
// while (0 == (USART1->SR & 0X40));// USART1->DR = (uint8_t)ch;// return ch;
//}#if 1
#pragma import(__use_no_semihosting)// 使用#pragma指令來導入__use_no_semihosting,表示不使用半主機模式。
// 半主機模式通常用于調試,允許在目標設備上運行時與主機進行交互。
struct __FILE
{int handle;
};FILE __stdout;// 定義一個全局變量__stdout,類型為FILE,用于標準輸出。
void _sys_exit(int x)
{x = x;
}int fputc(int ch, FILE *f)
{while (0 == (USART1->SR & 0X40));USART1->DR = (uint8_t)ch;return ch;
}
#endif
參考正點原子這樣修改解決了問題
map文件設計到的知識點
在分析嵌入式系統或單片機的 map
文件時,map
文件提供了詳細的內存布局和各個段(section)在內存中的分配情況。map
文件的內容對調試、優化和了解程序結構非常有幫助。
下面是對 map
文件中常見的幾個段(例如 .bss
段、.data
段以及其他段)的詳細分析。
1. .bss 段(Block Started by Symbol)
- 描述:
.bss
段是用于未初始化的全局變量和靜態變量的內存區域。在程序啟動時,這些變量會被清零或初始化為零。 - 特點:
.bss
段通常不占用實際的存儲空間。在編譯時,鏈接器并不為.bss
段中的變量分配實際的存儲空間,而是指示程序運行時為這些變量分配空間,并在程序加載時清零它們。- 它通常包括未顯式初始化為特定值的靜態變量和全局變量。
- 例子:
int global_var; // 默認值為 0 static int static_var; // 默認值為 0
- map 文件中的信息:
- 在
map
文件中,.bss
段會列出所有未初始化的全局和靜態變量及其所占的內存空間大小。由于這些變量會在程序啟動時被清零,因此.bss
段通常不會占用磁盤上的存儲空間。 - 大小:
map
文件中.bss
段的大小通常會較大,尤其是未初始化變量較多時。
- 在
2. .data 段
- 描述:
.data
段是用于存儲已初始化的全局變量和靜態變量的內存區域。與.bss
段不同,.data
段中的變量在編譯時就已經被賦予了初始值。 - 特點:
.data
段包含所有顯式初始化的全局和靜態變量。- 在程序啟動時,
.data
段的內容會從程序的可執行文件(或固件)加載到內存中。
- 例子:
int global_var = 10; // 已初始化的全局變量 static int static_var = 20; // 已初始化的靜態變量
- map 文件中的信息:
map
文件會列出.data
段中所有已初始化變量及其在內存中的起始地址和大小。- 大小:
.data
段的大小取決于已初始化的變量總量。
3. .text 段
- 描述:
.text
段是程序的代碼段,它存放了編譯后的機器代碼。在這個段中沒有變量,只有程序的指令。 - 特點:
- 代碼段通常是只讀的,因為程序代碼在執行時不應該被修改。
.text
段通常是程序中占用空間最多的部分,尤其是復雜程序和函數較多時。
- map 文件中的信息:
map
文件會顯示.text
段的起始地址、大小以及每個函數的實際內存位置。- 大小:代碼段的大小會受到程序中函數和指令的復雜度影響。
4. .heap 段
- 描述:
.heap
段用于動態分配內存(例如通過malloc
,calloc
,new
等函數分配的內存),通常位于程序的棧(.stack
)段之后。 - 特點:
- 動態內存分配的內存區域。
- 程序運行時的內存分配和釋放會發生在此區域。
- map 文件中的信息:
.heap
段的內存大小取決于程序運行時實際分配的內存量。map
文件會顯示.heap
段的起始地址、大小等信息。
5. .stack 段
- 描述:
.stack
段用于存儲函數調用時的局部變量、返回地址等信息。棧是一個動態分配的內存區域,隨著函數的調用和返回不斷增長和縮小。 - 特點:
- 棧的大小是由編譯器或鏈接器預設的,通常在
map
文件中可以看到棧的大小。 - 棧是一個向下增長的內存區域,通常在內存地址空間較高的位置。
- 棧的大小是由編譯器或鏈接器預設的,通常在
- map 文件中的信息:
map
文件會顯示棧的起始位置和大小等信息。棧的大小通常在linker script
或編譯器設置中配置。
6. .rodata 段(只讀數據段)
- 描述:
.rodata
段用于存儲程序中的常量數據(如字符串字面量、常量數組等),這些數據在程序運行期間不會被修改。 - 特點:
.rodata
是只讀的,不允許修改其中的數據。- 通常存儲字符串常量、全局常量等。
- map 文件中的信息:
map
文件會列出.rodata
段的內存占用情況,包括字符串常量等的內存地址和大小。
7. .init 和 .fini 段
- 描述:
.init
段包含程序初始化代碼,在程序啟動時被調用,通常用于設置初始化操作,如硬件初始化等。.fini
段包含程序結束時的清理代碼,通常在程序退出前進行資源清理。
- 特點:
- 這兩個段通常由編譯器或鏈接器自動處理。
- map 文件中的信息:
map
文件會顯示.init
和.fini
段的地址和大小。
8. 符號信息(符號表)
- 描述:
map
文件中還包含符號表,列出了程序中的所有符號(如變量、函數等)及其在內存中的地址。 - 特點:
- 符號表包含了所有變量和函數的名稱、類型和內存地址等信息,通常用于調試。
- map 文件中的信息:
- 每個符號(如全局變量、靜態變量、函數等)都會有一個對應的內存地址和大小。
總結
在 map
文件中,除了 .bss
、.data
和 .text
段外,還包含了棧、堆、只讀數據段等信息。每個段都有不同的作用,且在編譯和鏈接過程中被分配到不同的內存區域。通過查看 map
文件,你可以清晰地了解程序的內存分布、變量和函數的位置,進而優化內存使用和提高程序性能。
如題
//hal_systick_config_t cfg =
//{
// .clk_source = SYSTICK_CLK_HCLK_DIV8,
// .reload_value = 21000,
// .sys_clk_frequency = 168,
// .tick_callback = timer_ticks_count
//};static void systick_init(void)
{hal_systick_config_t cfg ={.clk_source = SYSTICK_CLK_HCLK_DIV8,.reload_value = 21000,.sys_clk_frequency = 168,.tick_callback = timer_ticks_count};hal_systick_init(&cfg);
}
這個代碼中,只要cfg結構體變量放到函數外面一切正常,只要放在函數內部則進入debug模式后需要三次run之后程序才會執行,把棧空間調大依然會出現這個問題
如何解決?
涉及到的知識點
一、堆棧初始值計算公式
STM32 的堆棧起始地址(即 SP 初始值)由以下公式決定:
SP_初始值=SRAM起始地址+RW_Data大小+ZI_Data大小+Stack_Size
??SRAM起始地址??:STM32F407 的 SRAM 起始地址為 0x20000000
??RW_Data(可讀寫數據)??:已初始化的全局變量和靜態變量
??ZI_Data(零初始化數據)??:未初始化的全局變量和靜態變量(默認填充為0)
??Stack_Size??:啟動文件中定義的棧大小(您設置為 0x400,即 1KB)
二、現象解釋
在您的案例中,??SP 初始值為 0x20000900?? 的具體計算邏輯如下:
??默認內存布局??:
SRAM 起始地址:0x20000000
程序中的全局變量(RW+ZI)總大小:假設為 0x900 字節
棧大小(Stack_Size):0x400 字節(1KB)
則 SP 初始值 = 0x20000000 + 0x900 + 0x400 = 0x20000D00
但實際觀察到的 SP 值為 0x20000900,這表明 ??RW+ZI 實際僅占用 0x500 字節??(0x20000900 - 0x20000000 - 0x400 = 0x500)。
??堆棧生長方向??:
STM32 的棧是 ??向下生長?? 的,SP 初始值指向棧頂(高地址),棧底為 SP - Stack_Size。因此:
棧頂地址:0x20000900
棧底地址:0x20000900 - 0x400 = 0x20000500
棧空間范圍:0x20000500 ~ 0x20000900。
根據.map文件分析,可以得出以下關于棧溢出問題的結論:
一、棧空間配置分析
??棧大小設置??
在startup_stm32f40_41xxx.o中明確顯示:
STACK 0x20000500 0x00000400 (1KB)
這與用戶設置的0x400(1024字節)完全一致。
??內存布局驗證??
棧的地址范圍:0x20000500 ~ 0x20000900
堆的地址范圍:0x20000300 ~ 0x20000500(512字節)
全局變量(ZI + RW Data):總占用2304字節(RW_IRAM1區域)。
二、棧溢出風險判斷
??棧使用量估算??
根據Image component sizes,ZI Data(零初始化數據)為2268字節,RW Data為36字節,總占用2304字節。
??關鍵點??:ZI Data包含全局變量和靜態變量,而棧和堆的地址范圍與ZI/RW區域相鄰。如果函數調用鏈中的局部變量過多,可能導致棧指針(SP)超出0x20000900,覆蓋其他內存區域。
??調試現象關聯??
用戶提到“變量放在函數內部時需要多次運行才成功”,這與棧溢出的典型現象(隨機性崩潰、HardFault)高度吻合。局部變量存儲在棧中,若超過0x400限制,會破壞中斷向量表或關鍵數據,導致程序異常。
??溢出檢測方法??
??靜態分析??:檢查函數調用層級和局部變量總大小。例如,若某函數定義了char buffer[1024],直接占滿棧空間,必然溢出。
??動態驗證??:在Keil調試器中觀察SP寄存器值是否超出0x20000900,或通過Memory窗口查看棧內存是否被意外改寫。
記錄遇到的坑
1、函數指針一定不能跑飛,加上保護,防止程序跑飛
2、局部變量或者局部數據,尤其局部大數組一定要static化,或者直接全局化,防止堆棧溢出
3、利用三元運算符替代if邏輯,程序看起來簡潔優雅
4、巧用#ifndef HAL_STATUS_T_DEFINED
#define HAL_STATUS_T_DEFINED
命令來解決重復定義