可執(zhí)行程序 -> cpu執(zhí)行第一條用戶代碼
這個流程中著重講述的是 HEX 文件如何被燒寫到 STM32 內(nèi)部的指定地址處。(燒寫到 STM32 中的可執(zhí)行文件不僅只有 HEX 格式,還有 axf、bin。針對不同格式的可執(zhí)行文件,用不同的工具進行燒寫)。
而本篇文章將要詳細地描述一個流程:
cpu執(zhí)行第一條用戶代碼 -> 調(diào)用 __main 函數(shù)-> __rt_entry -> main函數(shù)
這里需要注意一下,__main 是 c 庫中的一個函數(shù),和 main 函數(shù)是有區(qū)別的?。。?/p>
啟動文件內(nèi)容描述
上圖中的匯編關(guān)鍵字最好記住,因為比較常用。 在此基礎(chǔ)上,我們繼續(xù)深入一點。 DCD指令 STM32 啟動文件中使用 DCD 指令的目的是:達到 4GB 全范圍跳轉(zhuǎn)。 LDR 指令只能跳到當前 PC 4kB 范圍內(nèi),而 B 指令能跳轉(zhuǎn)到 32MB 范圍。 B . STM32 啟動文件中使用 b . 語句的作用就是:防止程序跑飛。 副作用:觸發(fā)了一個未知中斷的時候會卡死在中斷服務(wù)函數(shù)中,以至于你幾乎都找不到?。?!
注意:中斷服務(wù)函數(shù)全部都是在啟動文件中已經(jīng)定義好了,如果在外部文件中定義中斷服務(wù)函數(shù),名稱要和事先已經(jīng)定義好的中斷服務(wù)函數(shù)的名稱一樣,函數(shù)名稱的不同代表著地址的不同,因為函數(shù)名稱本質(zhì)就是地址?。?!
STM32啟動流程
獲取棧頂指針
跳轉(zhuǎn)到復(fù)位中斷函數(shù)
注意:當程序編譯完成之后,SP棧頂指針就已經(jīng)確定了。 MDK編譯程序的組成: Code:代碼域,它指的是編譯器生成的機器指令,這些內(nèi)容被存儲到 ROM 區(qū)。 RO-data:Read Only data,只讀數(shù)據(jù)域,它指程序中用到的只讀數(shù)據(jù),這些數(shù)據(jù)被存儲在 ROM 區(qū),因而程序不能修改其內(nèi)容。C語言中 const 關(guān)鍵字定義的變量就是典型的 RO-data。 RW-data:Read Write data,可讀寫數(shù)據(jù)域,它指初始化為”非0值“的可讀寫數(shù)據(jù),程序剛運行時,這些數(shù)據(jù)具有非0的初始值,且運行的時候它們會常駐在 RAM 區(qū),因而應(yīng)用程序可以修改其內(nèi)容。C 語言中定義的全局變量,且定義時賦予“非0值”給該變量進行初始化。 ZI-data:Zero Initialie data,即 0 初始化數(shù)據(jù),它指初始化為“0值”的可讀寫數(shù)據(jù)域。它與 RW-data 的區(qū)別是程序剛運行時這些數(shù)據(jù)初始值全都為 0,而后續(xù)運行過程與 RW-data 的性質(zhì)一樣,它們也常駐在 RAM 區(qū),因而應(yīng)用程序可以更改其內(nèi)容。例如 C 語言中使用定義的全局變量,且定義時賦予 “ 0 值” 給該變量進行初始化(若定義該變量時沒有賦予初始值,編譯器會把它當 ZI-data 來對待,初始化為 0)。 ZI-data 的??臻g(Stack)及堆空間(Heap):在 C 語言中,函數(shù)內(nèi)部定義的局部變量屬于棧空間,進入函數(shù)的時候會向??臻g申請內(nèi)存給局部變量,退出時釋放局部變量,歸還內(nèi)存空間。而使用 malloc 動態(tài)分配的變量屬于堆空間。在程序中的??臻g和堆空間都是屬于 ZI-data 區(qū)域的,這些空間都會被初始值化為 0 值。編譯器給出的 ZI-data 占用的空間值中包含了堆棧的大小(經(jīng)實際測試,若程序中完全沒有使用 malloc 動態(tài)申請堆空間,編譯器會優(yōu)化,不把堆空間計算在內(nèi))。 程序組件所屬的區(qū)域:
程序組件 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?所屬類別 ? ?
機器代碼指令 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Code ? ?
常量 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? RO-data ? ?
初值非0的全局變量 ? ? ? ? ? ? ? ? ? ? ?RW-data ? ?
初值為0的全局變量 ? ? ? ? ? ? ? ? ? ? ?ZI-data ? ?
局部變量 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ZI-data??臻g ? ?
使用 malloc 動態(tài)分配的空間 ? ? ? ZI-data堆空間
RW-data 和 ZI-data 它們僅僅是初始值不一樣而已,應(yīng)用程序具有靜止狀態(tài)和運行狀態(tài)。靜止態(tài)的程序被存儲在非易失存儲器中,如 STM32 的內(nèi)部 FLASH,因而系統(tǒng)掉電后也能正常保存但是當程序在運行狀態(tài)的時候,程序常常需要修改一些暫存數(shù)據(jù),由于運行速度的要求,這些數(shù)據(jù)往往存放在內(nèi)存中(RAM),掉電后這些數(shù)據(jù)會丟失。因此,程序在靜止與運行的時候它在存儲器中的表現(xiàn)是不一樣的。 程序狀態(tài)區(qū)域的組成;
程序狀態(tài)與區(qū)域 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 組成 ? ? 程序執(zhí)行時的只讀區(qū)域(RO) ? ? ? ? ?Code+RO-data ? ? 程序執(zhí)行時的可讀寫區(qū)域(RW) ? ? ?RW-data + ZI-data ? ? 程序存儲時占用的ROM區(qū) ? ? ? ? ? ? Code + RO-data + RW-data
最小啟動配置(加個雞腿)
注意:設(shè)置好 SP,就可以運行用戶程序。 編寫中斷向量表
編寫復(fù)位中斷函數(shù)
設(shè)置堆棧指針 跳轉(zhuǎn)到__main函數(shù) 至此,cpu執(zhí)行第一條用戶代碼 -> 調(diào)用__main函數(shù) 分析完畢,接下來是,__main函數(shù) -> __rt_entry -> main函數(shù)。 這里再次聲明一下:__main 函數(shù)是 c 庫中的一個函數(shù),和用戶編寫的 main 函數(shù)是有區(qū)別的!?。?/p>
必備知識
必備知識中主要是用到了.map文件,雙擊紅色箭頭所指向的區(qū)域就可以打開?。?!
用戶程序在FLASH中的組織架構(gòu)
上面兩張圖截取了鏡像文件在 FLASH 上的內(nèi)存分布。 從上面兩張圖可以知道,在程序的最開始處,存儲的是數(shù)據(jù)段,這個數(shù)據(jù)段就是中斷向量表,里面存儲這所有中斷函數(shù)的入口地址。 緊跟著的就是代碼段,代碼段包含了自己編寫的用戶代碼和庫函數(shù)。 之后又跟著數(shù)據(jù)段,這個數(shù)據(jù)段有個專有的名稱,叫做代碼常量區(qū),也就是你定義的 const 類型的全局變量(記住不是const 類型的局部變量,const 類型的局部變量還是存儲在棧區(qū))會存儲在這個區(qū)域。 特別注意,非常重要的知識點: 在代碼常量區(qū)后面還有一個區(qū),叫做讀寫數(shù)據(jù)區(qū),這個區(qū)域中的數(shù)據(jù)最終要被拷貝到 SRAM 中去,因為 FLASH 只能讀不能寫(事實上可以進行寫操作,只不過需要密鑰而已,參考手冊中有說明)而 SRAM 中的數(shù)據(jù)是可讀可寫的。 但是,.map 文件中并沒有提到,也就是說你從 .map 文件中是找不到這個區(qū)的,
你能看到的最后一項就是代碼常量區(qū),因此這個地方一般情況下很難發(fā)現(xiàn)到,只有深入 __main 函數(shù)之后才可以知道。
值得注意的是:
在代碼區(qū)中,不僅有Code、Data類型的數(shù)據(jù),還有 WPAD?。?! PAD 就是 padding 的意思,中文翻譯過來就是填充的意思。作用:進行4字節(jié)對齊,提高cpu的取指速率。 也就是說,無論是指令還是數(shù)據(jù),在內(nèi)存中都要4個字節(jié)對齊,所表現(xiàn)出來的特征就是: 地址的最低兩位都為 0,換成 16 進制來說,就是最后一個字母只能為 0、4、8、c。
用戶數(shù)據(jù)在SRAM中的組織架構(gòu)
在 SRAM 中,第一個區(qū)域叫做全局區(qū),也有人叫靜態(tài)區(qū)。你定義的全局變量(有初始值),靜態(tài)變量都存放在這個區(qū)域當中。 這里需要說明一下一個特例: 比如你定義了一個全局變量:int a; 沒有初始化的全局變量默認為 0,但要注意,并不是說沒有初始化的全局變量就屬于 .bss 段(網(wǎng)上有很多的博客都說錯了),它還是屬于全局區(qū),它的值是編譯器賦值給它的?。?! 緊跟著的就是.bss段。
注意:.bss 段不被包含在可執(zhí)行文件當中
定義的未初始化全局數(shù)組,未初始化的靜態(tài)全局數(shù)組等等保存在 .bss 段。 接下來就是堆和棧,因為堆向上生長,棧向下生長,因此堆在棧的前面。 此時,我們得到一個非常重要的結(jié)論:棧頂指針的值 = RW-data + ZI-data。
大家可以想一下,為什么。 還有,由于當一個程序生成可執(zhí)行文件之后,棧頂指針的值就確定了。 那也就是說,從棧頂指針處,到 SRAM 最后一個存儲單元都處于未使用狀態(tài),也就是說,有一部分內(nèi)存我們是沒有使用的,這里需要注意?。?!
加載地址 鏈接地址 運行地址 存儲地址
加載地址:將指令或數(shù)據(jù)從地址 A 拷貝到地址 B,地址 A 就是加載地址。
鏈接地址:由鏈接腳本文件指出,鏈接的時候確定。
運行地址:程序在內(nèi)存中運行時候的地址。
存儲地址:指令或數(shù)據(jù)在 flash 中存放的存儲地址,就是存儲地址。
這里需要說明一下:
鏈接地址是靜態(tài)的,在程序鏈接的時候確定。
運行地址是動態(tài)的,因為當你使用位置無關(guān)碼(后面會提到)將程序從 A 地址拷貝到 B 地址處,那么運行地址就發(fā)生了改變。
存儲地址就是加載地址,沒有區(qū)別!??!
代碼重定向 程序或數(shù)據(jù)的鏈接地址要和運行地址一致,但往往程序或數(shù)據(jù)的存儲地址(加載地址)和運行地址不一樣,因此需要代碼重定向。 代碼重定向:使用位置無關(guān)碼將用戶程序或數(shù)據(jù)從存儲地址拷貝到運行地址。 用一句很精確的話來描述代碼重定向:使邏輯地址與實際物理地址一一對應(yīng)的過程。 這篇博客非常詳細地描述了代碼重定向的過程,讀者特別需要注意的就是:MCU和MPU代碼重定向的區(qū)別?。?! 位置無關(guān)碼 當程序或數(shù)據(jù)的鏈接地址和運行地址不一樣的時候,此時只有位置無關(guān)碼才能夠正確被執(zhí)行 位置無關(guān)碼:依賴于程序當前運行的PC值,進行相對的跳轉(zhuǎn),導(dǎo)致的結(jié)果就是,無論代碼在哪,總能達到指令正常運行的目的,因此是位置無關(guān)的。 位置有關(guān)碼:不依賴當前PC值,是絕對跳轉(zhuǎn),只有程序運行在鏈接地址處時,才能達到指令的正常目的,因此是位置有關(guān)系的。
__main函數(shù)
作用: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.(我們后面會自己實現(xiàn) __main函數(shù))
注意:__main 函數(shù)不會將 RO 段數(shù)據(jù)拷貝到執(zhí)行地址處,雖然官方說明了
_rt_entry 函數(shù)
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 庫函數(shù)是沒有源文件,都已經(jīng)編譯完成了。 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.
自己實現(xiàn) __main 函數(shù)
消除警告 提示:程序的首地址并不和程序的入口地址等效。 注意:ARM 匯編語法 entry 是一個程序的入口地址,GNU 匯編語法 start 是一個程序的入口地址。 我們已自己實現(xiàn) __main 函數(shù),ENTRY 已沒有實質(zhì)作用, 但為了避免 KEIL 警告,這里加上。
自己實現(xiàn)__rt_entry函數(shù)
你覺得你行嗎?你知道要多少行代碼嗎,并且,沒必要?。。?/p>
問題思考
為什么我們可以自己編寫 __main 和 __rt_entry 因為庫函數(shù)里面的 W__main 函數(shù) 和 __rt_entry 函數(shù)是弱函數(shù)。
弱函數(shù)定義時需要寫紅色箭頭所指向的關(guān)鍵字。 當一個用戶程序運行完以后,會出現(xiàn)什么情況 MCU的程序執(zhí)行結(jié)束后去哪兒了
總結(jié)
_ _main函數(shù) -> __rt_entry函數(shù) -> main函數(shù) 介紹完畢。 本系列文章流程: 可執(zhí)行程序 -> cpu執(zhí)行第一條用戶代碼的流程 -> _ _main函數(shù) -> __rt_entry函數(shù) -> main函數(shù) 詳細地闡述了可執(zhí)行文件是如何被加載到 FLASH上,以及編寫的用戶程序(main函數(shù))被調(diào)用之前經(jīng)歷了哪些步驟。 如果你對這些步驟了然于胸的時候,那么恭喜你,你已經(jīng)很強了,大部分人是學(xué)不到這么深的,就算工作了很多年!?。?希望本系列的博文能夠?qū)δ阌兴鶐椭。。?最后,希望大家能夠?qū)W有所成,未來可期。?
?
編輯:黃飛
?
評論
查看更多