單片機main函數退出后發生什么——以stm32為例


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的安排?是在哪一步實施的?文中的實驗具有普適性嗎?等等疑問還等待着解答。

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


歡迎轉載,請注明作者與原文鏈接。

作者:胡小安

原文鏈接https://www.cnblogs.com/huxiaoan/p/15821662.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM