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