微控制器main函式退出後發生什麼——以stm32為例

胡小安發表於2022-01-19

STM32:main函式退出後發生什麼?

我們都在說微控制器要執行在無限迴圈裡,不能退出,可退出之後會發生什麼?

討論STM32啟動過程的文章數不勝數,可main函式結束之後會發生什麼卻少有討論。

幾日前突然想到這個問題,便開始了探究。

如果不想看冗長的調查和實驗過程,可以直接到文章底部看結論,也有流程圖版哦。



網上搜尋

可能因為大家不太關心這種情況,我沒有找到有關論述微控制器main函式退出的文章。不過在ST Community、阿莫BBS、StackOverflow看到有人在問同樣的問題,下面摘錄了一些不同角度的回答:

  1. C語言環境角度,三種可能性
    • 編譯器在main函式後加入隱性的無限迴圈
    • 編譯器在main外面新增一層無限迴圈
    • CPU繼續向下取址執行(也就是跑飛了)
  2. 微控制器設計角度,退出會引發異常、事件等
  3. 實際測試,網友們得到的結果卻不太一樣
    • 有的會自動迴圈,像是自動復位了
    • 有的會迴圈同一段彙編

可以看出,答案眾說紛紜,並沒有權威性,於是就轉向了最權威的資料:Keil手冊,arm官方工具鏈文件。

文件查閱

為了尋找main外面的呼叫情況,我們要從熟悉的啟動程式碼開始:

Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

我們知道,是__main呼叫了使用者main函式,在手冊1.8.1 Initialization of the execution environment and execution of the application這一小節,概述了__main的作用:

  1. 複製RO和RW段的內容,必要的話進行解壓縮
  2. 初始化ZI段(置零)
  3. 呼叫__rt_entry

那這個__rt_entry是十分的重要啊!

按圖索驥,__rt_entry的功能有以下幾點:

  1. 呼叫函式初始化堆疊
  2. 初始化C庫,runtime
  3. 呼叫使用者的main
  4. Calls exit() with the value returned by main()

情況不唯一,第四步的exit()可以換為另外兩個退出函式,他們三個退出函式的關係在後面會提到。

情況變得明朗起來,只要找到這個exit()呼叫的實現即可。我在stdlib.h中找到了exit的宣告:

extern _ARMABI_NORETURN void exit(int /*status*/);
   /*註釋有刪減,刪掉了不少次要內容,有興趣可以去看一看。
    * First, all functions registered by the atexit function are called.
    * Next, all open output streams are flushed, all open streams are closed,
    * and all files created by the tmpfile function are removed.
    * Finally, control is returned to the host environment.
    */

總結下來有三個功能:1. 呼叫之前註冊過的atexit函式 2. 關閉C執行時 3. 向宿主環境上交控制權。

然而具體實現細節還是未知的,我們回到__rt_entry的文件中看看:

image-20220117153954541

最後一步,必須呼叫exit__rt_exit_sys_exit三個中的一個。然而仔細觀察他們三個的功能,是不是能察覺出一絲重複的意味。在功能上,exit包含__rt_exit包含_sys_exit,顯然他們三個不會是毫無關聯的。

在閱讀完所有相關文件後,我們能得出結論:exit呼叫__rt_exit呼叫_sys_exit,後面實驗中的彙編也印證了這一點。

然而,其中的_sys_exit是不是看起來很眼熟呢?相信用過STM32的朋友都瞭解串列埠列印除錯與printf函式重定向(只討論不使用microlib的情況),其中會有這樣一段函式定義:

void _sys_exit(int x) //避免半主機模式
{ 
	x = x; 
} 

如果閱讀了1.6.4 Using the libraries in a nonsemihosting environment這一節,我們就會發現_sys_exit是典型的依賴半主機模式的呼叫。因為啟動程式碼中的函式一路呼叫會呼叫到_sys_exit上去,所以在非半主機模式下我們需要自己提供它的定義。

Semihosting,半主機模式會把標準C庫中的一些應該提供的函式使用特定的指令交給除錯主機來實現。由1.8.5 Direct semihosting C library function dependencies可知這些函式包括:

_sys_exit _sys_close _sys_open _sys_write等,在半主機模式下,對這些函式直接或者間接的呼叫將轉化為特定的指令。在非半主機模式下,就需要手動實現被呼叫的函式。

半主機作為一種除錯手段,聽起來非常誘人,ARM自己的Keil MDK竟然不支援。既然半主機模式影響了必然會被呼叫_sys_exit,那就會影響到main函式退出之後的動向。在下一節的實測中,也確實體現出了巨大的差異。

實驗測試

晶片:STM32F407ZGT6

模擬器:DAP-Link

環境:ARMCC V5.06 update 6 ,Keil 5.25.2.0 , -O0

main函式內容如下:

int main(void){
	GPIO_InitTypeDef GPIO_Initure;
     
    HAL_Init();                    	 			//初始化HAL庫    
    Stm32_Clock_Init(336,8,2,7);   				//設定時鐘,168Mhz
    __HAL_RCC_GPIOF_CLK_ENABLE();           	//開啟GPIOF時鐘
    GPIO_Initure.Pin=GPIO_PIN_9|GPIO_PIN_10; 	//PF9,10
    GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;  	//推輓輸出
    GPIO_Initure.Pull=GPIO_PULLUP;          	//上拉
    GPIO_Initure.Speed=GPIO_SPEED_HIGH;    	 	

	//開燈
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);	
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
    HAL_Delay(1000);
	//關燈
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);		
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);		
    HAL_Delay(1000);
}

PF9和PF10是開發板上兩顆LED燈,能提供直觀展示。

非半主機

現象:兩顆LED不斷閃動,就像處於迴圈之中。

為了找出原因,自然是要開始打斷點+單步除錯彙編。

Image

此時的彙編是這樣的:

image-20220118233158298

繼續向下取址的話,接下來會彈棧,也將返回到呼叫main的函式__rt_entry中:

image-20220118233421880

這也印證了之前的推斷,在預設情況下,呼叫的是exit。實際執行與之前分析一致,exit呼叫__rt_exit呼叫_sys_exit

__rt_exit呼叫_sys_exit

自定義的_sys_exit

最後呼叫的,我們自己定義的_sys_exit,可以看出x=x被編譯器優化成為一句空指令。

重點在於接下來,按照手冊上說,_sys_exit將會把控制權交回宿主環境,此時C執行庫已經被關閉。然而下一句彙編BX lr直接將函式返回0x08000227,也就是__rt_exit函式呼叫_sys_exit的下文。在上上張圖中,可以發現程式碼又回到了熟悉的啟動程式碼,接下來,時鐘、堆疊、C庫依次初始化,main函式被呼叫,形成迴圈

這就是退出主函式後表現為迴圈的原因。

半主機

如果想進入半主機模式,我們可以將#pragma import(__use_no_semihosting) 這句巨集刪除,之後把自定義的_sys_exit等函式註釋掉,再進行編譯、下載、除錯。

現象:LED燈亮滅一次,無後序現象。

啟動以及退出流程與非半主機完全一樣,除了在呼叫_sys_exit時會變為相應的核心特定指令。ARM處理器在進入半主機模式時會呼叫trap instruction,對於所有的Cortex-M微處理器來說,這個指令是BKPT 0xAB。緊接著,就進入了跳轉到自己的死迴圈。

Image

至此,微控制器陷入空白死迴圈,形成了前文所說的執行一次現象。

結論

通過查閱官方文件,以及除錯實測,我們能得出結論:

在關閉半主機模式下,STM32的使用者main函式退出了,微控制器將會復位,形成迴圈的效果。開啟半主機模式下,如果退出主程式,會在空迴圈卡死,表現為只會執行一遍主函式內容。補充一點,如果使用微庫(microlib),文件中明文禁止退出main函式。

使用流程圖表示如下:

STM32啟動退出流程

這篇文章所討論的退出主函式,對於沒有OS的微控制器來說,可以說是一種未定義行為,本身是不安全的、不被推薦的。以上的討論與實驗。雖然實用性不高,但在學習過程中仍有不少的收穫。


這個主題原本是偶然想到的,花費了一些精力,算把這個問題弄清楚一些了。同時,在這個過程中產生了更多的疑問:將啟動程式碼放置在_sys_exit之後是ARM還是ST的安排?是在哪一步實施的?文中的實驗具有普適性嗎?等等疑問還等待著解答。

技術新人,水平有限,希望各位前輩、高人不吝賜教,如有錯誤請一定指出。更多嵌入式原創文章可以來公眾號,來找我聊聊天吧:

微控制器main函式退出後發生什麼——以stm32為例

歡迎轉載,請註明作者與原文連結。

作者:胡小安

原文連結https://www.cnblogs.com/huxiaoan/p/15821662.html

相關文章