可執行程序 -> cpu執行第一條用戶代碼
這個流程中著重講述的是 HEX 文件如何被燒寫到 STM32 內部的指定地址處。(燒寫到 STM32 中的可執行文件不僅只有 HEX 格式,還有 axf、bin。針對不同格式的可執行文件,用不同的工具進行燒寫)。
而本篇文章將要詳細地描述一個流程:
cpu執行第一條用戶代碼 -> 調用 __main 函數-> __rt_entry -> main函數
這里需要注意一下,__main 是 c 庫中的一個函數,和 main 函數是有區別的?。?!
啟動文件內容描述
上圖中的匯編關鍵字最好記住,因為比較常用。 在此基礎上,我們繼續深入一點。 DCD指令 STM32 啟動文件中使用 DCD 指令的目的是:達到 4GB 全范圍跳轉。 LDR 指令只能跳到當前 PC 4kB 范圍內,而 B 指令能跳轉到 32MB 范圍。 B . STM32 啟動文件中使用 b . 語句的作用就是:防止程序跑飛。 副作用:觸發了一個未知中斷的時候會卡死在中斷服務函數中,以至于你幾乎都找不到?。?!
注意:中斷服務函數全部都是在啟動文件中已經定義好了,如果在外部文件中定義中斷服務函數,名稱要和事先已經定義好的中斷服務函數的名稱一樣,函數名稱的不同代表著地址的不同,因為函數名稱本質就是地址?。?!
STM32啟動流程
獲取棧頂指針
跳轉到復位中斷函數
注意:當程序編譯完成之后,SP棧頂指針就已經確定了。 MDK編譯程序的組成: Code:代碼域,它指的是編譯器生成的機器指令,這些內容被存儲到 ROM 區。 RO-data:Read Only data,只讀數據域,它指程序中用到的只讀數據,這些數據被存儲在 ROM 區,因而程序不能修改其內容。C語言中 const 關鍵字定義的變量就是典型的 RO-data。 RW-data:Read Write data,可讀寫數據域,它指初始化為”非0值“的可讀寫數據,程序剛運行時,這些數據具有非0的初始值,且運行的時候它們會常駐在 RAM 區,因而應用程序可以修改其內容。C 語言中定義的全局變量,且定義時賦予“非0值”給該變量進行初始化。 ZI-data:Zero Initialie data,即 0 初始化數據,它指初始化為“0值”的可讀寫數據域。它與 RW-data 的區別是程序剛運行時這些數據初始值全都為 0,而后續運行過程與 RW-data 的性質一樣,它們也常駐在 RAM 區,因而應用程序可以更改其內容。例如 C 語言中使用定義的全局變量,且定義時賦予 “ 0 值” 給該變量進行初始化(若定義該變量時沒有賦予初始值,編譯器會把它當 ZI-data 來對待,初始化為 0)。 ZI-data 的棧空間(Stack)及堆空間(Heap):在 C 語言中,函數內部定義的局部變量屬于棧空間,進入函數的時候會向??臻g申請內存給局部變量,退出時釋放局部變量,歸還內存空間。而使用 malloc 動態分配的變量屬于堆空間。在程序中的棧空間和堆空間都是屬于 ZI-data 區域的,這些空間都會被初始值化為 0 值。編譯器給出的 ZI-data 占用的空間值中包含了堆棧的大?。ń泴嶋H測試,若程序中完全沒有使用 malloc 動態申請堆空間,編譯器會優化,不把堆空間計算在內)。 程序組件所屬的區域:
程序組件 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?所屬類別 ? ?
機器代碼指令 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Code ? ?
常量 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? RO-data ? ?
初值非0的全局變量 ? ? ? ? ? ? ? ? ? ? ?RW-data ? ?
初值為0的全局變量 ? ? ? ? ? ? ? ? ? ? ?ZI-data ? ?
局部變量 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ZI-data??臻g ? ?
使用 malloc 動態分配的空間 ? ? ? ZI-data堆空間
RW-data 和 ZI-data 它們僅僅是初始值不一樣而已,應用程序具有靜止狀態和運行狀態。靜止態的程序被存儲在非易失存儲器中,如 STM32 的內部 FLASH,因而系統掉電后也能正常保存但是當程序在運行狀態的時候,程序常常需要修改一些暫存數據,由于運行速度的要求,這些數據往往存放在內存中(RAM),掉電后這些數據會丟失。因此,程序在靜止與運行的時候它在存儲器中的表現是不一樣的。 程序狀態區域的組成;
程序狀態與區域 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 組成 ? ? 程序執行時的只讀區域(RO) ? ? ? ? ?Code+RO-data ? ? 程序執行時的可讀寫區域(RW) ? ? ?RW-data + ZI-data ? ? 程序存儲時占用的ROM區 ? ? ? ? ? ? Code + RO-data + RW-data
最小啟動配置(加個雞腿)
注意:設置好 SP,就可以運行用戶程序。 編寫中斷向量表
編寫復位中斷函數
設置堆棧指針 跳轉到__main函數 至此,cpu執行第一條用戶代碼 -> 調用__main函數 分析完畢,接下來是,__main函數 -> __rt_entry -> main函數。 這里再次聲明一下:__main 函數是 c 庫中的一個函數,和用戶編寫的 main 函數是有區別的!??!
必備知識
必備知識中主要是用到了.map文件,雙擊紅色箭頭所指向的區域就可以打開!!!
用戶程序在FLASH中的組織架構
上面兩張圖截取了鏡像文件在 FLASH 上的內存分布。 從上面兩張圖可以知道,在程序的最開始處,存儲的是數據段,這個數據段就是中斷向量表,里面存儲這所有中斷函數的入口地址。 緊跟著的就是代碼段,代碼段包含了自己編寫的用戶代碼和庫函數。 之后又跟著數據段,這個數據段有個專有的名稱,叫做代碼常量區,也就是你定義的 const 類型的全局變量(記住不是const 類型的局部變量,const 類型的局部變量還是存儲在棧區)會存儲在這個區域。 特別注意,非常重要的知識點: 在代碼常量區后面還有一個區,叫做讀寫數據區,這個區域中的數據最終要被拷貝到 SRAM 中去,因為 FLASH 只能讀不能寫(事實上可以進行寫操作,只不過需要密鑰而已,參考手冊中有說明)而 SRAM 中的數據是可讀可寫的。 但是,.map 文件中并沒有提到,也就是說你從 .map 文件中是找不到這個區的,
你能看到的最后一項就是代碼常量區,因此這個地方一般情況下很難發現到,只有深入 __main 函數之后才可以知道。
值得注意的是:
在代碼區中,不僅有Code、Data類型的數據,還有 WPAD?。。?PAD 就是 padding 的意思,中文翻譯過來就是填充的意思。作用:進行4字節對齊,提高cpu的取指速率。 也就是說,無論是指令還是數據,在內存中都要4個字節對齊,所表現出來的特征就是: 地址的最低兩位都為 0,換成 16 進制來說,就是最后一個字母只能為 0、4、8、c。
用戶數據在SRAM中的組織架構
在 SRAM 中,第一個區域叫做全局區,也有人叫靜態區。你定義的全局變量(有初始值),靜態變量都存放在這個區域當中。 這里需要說明一下一個特例: 比如你定義了一個全局變量:int a; 沒有初始化的全局變量默認為 0,但要注意,并不是說沒有初始化的全局變量就屬于 .bss 段(網上有很多的博客都說錯了),它還是屬于全局區,它的值是編譯器賦值給它的?。。?緊跟著的就是.bss段。
注意:.bss 段不被包含在可執行文件當中
定義的未初始化全局數組,未初始化的靜態全局數組等等保存在 .bss 段。 接下來就是堆和棧,因為堆向上生長,棧向下生長,因此堆在棧的前面。 此時,我們得到一個非常重要的結論:棧頂指針的值 = RW-data + ZI-data。
大家可以想一下,為什么。 還有,由于當一個程序生成可執行文件之后,棧頂指針的值就確定了。 那也就是說,從棧頂指針處,到 SRAM 最后一個存儲單元都處于未使用狀態,也就是說,有一部分內存我們是沒有使用的,這里需要注意!!!
加載地址 鏈接地址 運行地址 存儲地址
加載地址:將指令或數據從地址 A 拷貝到地址 B,地址 A 就是加載地址。
鏈接地址:由鏈接腳本文件指出,鏈接的時候確定。
運行地址:程序在內存中運行時候的地址。
存儲地址:指令或數據在 flash 中存放的存儲地址,就是存儲地址。
這里需要說明一下:
鏈接地址是靜態的,在程序鏈接的時候確定。
運行地址是動態的,因為當你使用位置無關碼(后面會提到)將程序從 A 地址拷貝到 B 地址處,那么運行地址就發生了改變。
存儲地址就是加載地址,沒有區別!!!
代碼重定向 程序或數據的鏈接地址要和運行地址一致,但往往程序或數據的存儲地址(加載地址)和運行地址不一樣,因此需要代碼重定向。 代碼重定向:使用位置無關碼將用戶程序或數據從存儲地址拷貝到運行地址。 用一句很精確的話來描述代碼重定向:使邏輯地址與實際物理地址一一對應的過程。 這篇博客非常詳細地描述了代碼重定向的過程,讀者特別需要注意的就是:MCU和MPU代碼重定向的區別?。?! 位置無關碼 當程序或數據的鏈接地址和運行地址不一樣的時候,此時只有位置無關碼才能夠正確被執行 位置無關碼:依賴于程序當前運行的PC值,進行相對的跳轉,導致的結果就是,無論代碼在哪,總能達到指令正常運行的目的,因此是位置無關的。 位置有關碼:不依賴當前PC值,是絕對跳轉,只有程序運行在鏈接地址處時,才能達到指令的正常目的,因此是位置有關系的。
__main函數
作用:Initialization of the execution environment and execution of the application You can customize execution intialization by defining your own __main that branches to __rt_entry. The entry point of a program is at __main in the C library where library code:
Copies non-root (RO(不會拷貝,官方提供和實際實踐有出入) and RW) execution regions from their load addresses to their execution addresses. Also, if any data sections are compressed, they are decompressed from the load address to the execution address.
Zeroes ZI regions.
Branches to __rt_entry.
If you do not want the library to perform these actions, you can define your own __main that branches to __rt_entry.(我們后面會自己實現 __main函數)
注意:__main 函數不會將 RO 段數據拷貝到執行地址處,雖然官方說明了
_rt_entry 函數
procedure The library function __rt_entry() runs the program as follows:
Sets up the stack and the heap by one of a number of means that include calling __user_setup_stackheap(), ?calling ?__rt_stackheap_init(), ?or loading the absolute addresses of scatter-loaded regions.
Calls __rt_lib_init() to initialize referenced library functions, initialize the locale and, if necessary, set up argc and argv for main().This function is called immediately after __rt_stackheap_init() and is passed an initial chunk of memory to use as a heap. This function is the standard ARM C library initialization function and it must not be reimplemented.
Calls main(), the user-level root of the application.
From main(), your program might call, among other things, library functions.
Calls exit() with the value returned by main().
entry 的是 ARM 匯編語法中程序的入口地址,GNU Assember 語法中 start 是程序的入口地址 __rt_lib 庫函數是沒有源文件,都已經編譯完成了。 The symbol __rt_entry is the starting point for a program using the ARM C library. Control passes to __rt_entry after all scatter-loaded regions have been relocated to their execution addresses. Usage
The default implementation of __rt_entry:
Sets up the heap and stack.
Initializes the C library by calling __rt_lib_init.(ARMc庫里面全面都是 .b ?.l 形式的庫,沒有源碼)
Calls main().
Shuts down the C library, by calling __rt_lib_shutdown.
Exits.
__rt_entry must end with a call to one of the following functions:
exit()
Calls atexit()- registered functions and shuts down the library.
__rt_exit()
Shuts down the library but does not call atexit() functions.
_sys_exit()
Exits directly to the execution environment. It does not shut down the library and does not call atexit() functions.
自己實現 __main 函數
消除警告 提示:程序的首地址并不和程序的入口地址等效。 注意:ARM 匯編語法 entry 是一個程序的入口地址,GNU 匯編語法 start 是一個程序的入口地址。 我們已自己實現 __main 函數,ENTRY 已沒有實質作用, 但為了避免 KEIL 警告,這里加上。
自己實現__rt_entry函數
你覺得你行嗎?你知道要多少行代碼嗎,并且,沒必要!?。?/p>
問題思考
為什么我們可以自己編寫 __main 和 __rt_entry 因為庫函數里面的 W__main 函數 和 __rt_entry 函數是弱函數。
弱函數定義時需要寫紅色箭頭所指向的關鍵字。 當一個用戶程序運行完以后,會出現什么情況 MCU的程序執行結束后去哪兒了
總結
_ _main函數 -> __rt_entry函數 -> main函數 介紹完畢。 本系列文章流程: 可執行程序 -> cpu執行第一條用戶代碼的流程 -> _ _main函數 -> __rt_entry函數 -> main函數 詳細地闡述了可執行文件是如何被加載到 FLASH上,以及編寫的用戶程序(main函數)被調用之前經歷了哪些步驟。 如果你對這些步驟了然于胸的時候,那么恭喜你,你已經很強了,大部分人是學不到這么深的,就算工作了很多年!!! 希望本系列的博文能夠對你有所幫助?。。?最后,希望大家能夠學有所成,未來可期。?
?
編輯:黃飛
?
評論
查看更多