前言:
為了友善檢視部落格,特意申請了一個公衆号,附上二維碼,有興趣的朋友可以關注,和我一起讨論學習,一起享受技術,一起成長。

本文轉載自:第48章 MDK的編譯過程及檔案類型全解—零死角玩轉STM32-F429系列
1. 簡介
本章參考資料:MDK 的幫助手冊《ARM Development Tools》,點選 MDK 界面的 “help->uVision Help” 菜單可打開該檔案。關于 ELF 檔案格式,參考配套資料裡的《ELF檔案格式》檔案。
在本章中講解了非常多的檔案類型,學習時請跟着教程的節奏,打開實際工程中的檔案來了解。
相信您已經非常熟練地使用 MDK 建立應用程式了,平時使用 MDK 編寫源代碼,然後編譯生成機器碼,再把機器碼下載下傳到 STM32 晶片上運作,但是這個編譯、下載下傳的過程 MDK 究竟做了什麼工作?它編譯後生成的各種檔案又有什麼作用?本章節将對這些過程進行講解,了解編譯及下載下傳過程有助于了解晶片的工作原理,這些知識對制作 IAP(bootloader) 以及讀寫控制器内部 FLASH 的應用時非常重要。
2. 編譯過程
2.1 編譯過程簡介
首先我們簡單了解下 MDK 的編譯過程,它與其它編譯器的工作過程是類似的,該過程見下圖。
編譯過程生成流程:
(1) 編譯:MDK 軟體使用的編譯器是 armcc 和 armasm,它們根據每個 c/c++ 和彙編源檔案編譯成對應的以 “.o” 為字尾名的對象檔案 (Object Code,也稱目标檔案),其内容主要是從源檔案編譯得到的機器碼,包含了代碼、資料以及調試使用的資訊;
(2) 連結:連結器 armlink 把各個 .o 檔案及庫檔案連結成一個映像檔案 “.axf” 或 “.elf”;
(3) 格式轉換:一般來說 Windows 或 Linux 系統使用連結器直接生成可執行映像檔案 elf 後,核心根據該檔案的資訊加載後,就可以運作程式了,但在單片機平台上,需要把該檔案的内容加載到晶片上,是以還需要對連結器生成的 elf 映像檔案利用格式轉換器 fromelf 轉換成 “.bin” 或 “.hex” 檔案,交給下載下傳器下載下傳到晶片的FLASH 或 ROM 中。
2.2 具體工程中的編譯過程
下面我們打開"多彩流水燈"的工程,以它為例進行講解,其它工程的編譯過程也是一樣的,隻是檔案有差異。打開工程後,點選 MDK 的 “rebuild” 按鈕,它會重新建構整個工程,建構的過程會在 MDK 下方的 “Build Output” 視窗輸出提示資訊,見下圖。
建構工程的提示輸出主要分 6 個部分,說明如下:
(1) 提示資訊的第一部分說明建構過程調用的編譯器。圖中的編譯器名字是 “V5.06(build 20)”,後面附帶了該編譯器所在的檔案夾。在電腦上打開該路徑,可看到該編譯器包含上圖中的各個編譯工具,如 armar、armasm、armcc、armlink及 fromelf。armar 是用于把 .o 檔案打包成 lib 檔案的。
(2)使用 armasm 編譯彙編檔案。圖中列出了編譯 startup 啟動檔案時的提示,編譯後每個彙編源檔案都對應有一個獨立的 .o 檔案。
(3)使用 armcc 編譯 c/c++ 檔案。圖中列出了工程中所有的 c/c++ 檔案的提示,同樣地,編譯後每個 c/c++ 源檔案都對應有一個獨立的 .o 檔案。
(4) 使用 armlink 連結對象檔案,根據程式的調用把各個 .o 檔案的内容連結起來,最後生成程式的 axf 映像檔案,并附帶程式各個域大小的說明,包括 Code、RO-data、RW-data 及 ZI-data 的大小。
(5)使用 fromelf 生成下載下傳格式檔案,它根據 axf 映像檔案轉化成 hex 檔案,并列出編譯過程出現的錯誤 (Error) 和警告 (Warning) 數量。
(6) 最後一段提示給出了整個建構過程消耗的時間。
建構完成後,可在工程的 “Output” 及 “Listing” 目錄下找到由以上過程生成的各種檔案,見下圖。
可以看到,每個 C 源檔案都對應生成了 .o、.d 及 .crf 字尾的檔案,還有一些額外的 .dep、.hex、.axf、.htm、.lnp、.sct、.lst 及 .map 檔案。
3. 程式的組成、存儲與運作
3.1 CODE、RO、RW、ZI Data域及堆棧空間
在工程的編譯提示輸出資訊中有一個語句 “Program Size:Code=xx RO-data=xx RW-data=xx ZI-data=xx”,它說明了程式各個域的大小,編譯後,應用程式中所有具有同一性質的資料(包括代碼)被歸到一個域,程式在存儲或運作的時候,不同的域會呈現不同的狀态,這些域的意義如下:
(1) Code:即代碼域,它指的是編譯器生成的機器指令,這些内容被存儲到 ROM 區。
(2)RO-data:Read Only data,即隻讀資料域,它指程式中用到的隻讀資料,這些資料被存儲在 ROM 區,因而程式不能修改其内容。例如 C 語言中 const 關鍵字定義的變量就是典型的 RO-data。
(3)RW-data:Read Write data,即可讀寫資料域,它指初始化為"非 0 值"的可讀寫資料,程式剛運作時,這些資料具有非 0 的初始值,且運作的時候它們會常駐在 RAM 區,因而應用程式可以修改其内容。例如 C 語言中使用定義的全局變量,且定義時賦予"非 0 值"給該變量進行初始化。
(4)ZI-data:Zero Initialie data,即 0 初始化資料,它指初始化為 " 0 值" 的可讀寫資料域,它與 RW-data 的差別是程式剛運作時這些資料初始值全都為 0,而後續運作過程與 RW-data 的性質一樣,它們也常駐在 RAM 區,因而應用程式可以更改其内容。例如 C 語言中使用定義的全局變量,且定義時賦予 “0 值” 給該變量進行初始化(若定義該變量時沒有賦予初始值,編譯器會把它當 ZI-data 來對待,初始化為 0);
(5)ZI-data 的棧空間 (Stack) 及堆空間 (Heap):在 C 語言中,函數内部定義的局部變量屬于棧空間,進入函數的時候從向棧空間申請記憶體給局部變量,退出時釋放局部變量,歸還記憶體空間。而使用 malloc 動态配置設定的變量屬于堆空間。在程式中的棧空間和堆空間都是屬于 ZI-data 區域的,這些空間都會被初始值化為 0 值。編譯器給出的 ZI-data 占用的空間值中包含了堆棧的大小(經實際測試,若程式中完全沒有使用 malloc 動态申請堆空間,編譯器會優化,不把堆空間計算在内)。
綜上所述,以程式的組成構件為例,它們所屬的區域類别見下表:
3.2 程式的存儲與運作
RW-data 和 ZI-data 它們僅僅是初始值不一樣而已,為什麼編譯器非要把它們區分開?這就涉及到程式的存儲狀态了,應用程式具有靜止狀态和運作狀态。靜止狀态的程式被存儲在非易失存儲器中,如 STM32 的内部 FLASH,因而系統掉電後也能正常儲存。但是當程式在運作狀态的時候,程式常常需要修改一些暫存資料,由于運作速度的要求,這些資料往往存放在記憶體中 (RAM),掉電後這些資料會丢失。是以,程式在靜止與運作的時候它在存儲器中的表現是不一樣的,見下圖 。
圖中的左側是應用程式的存儲狀态,右側是運作狀态,而上方是 RAM 存儲器區域,下方是R OM 存儲器區域。
程式在存儲狀态時,RO 節 (RO section) 及 RW 節都被儲存在 ROM 區。當程式開始運作時,核心直接從 ROM 中讀取代碼,并且在執行主體代碼前,會先執行一段加載代碼,它把 RW 節資料從 ROM 複制到 RAM,并且在 RAM 加入 ZI 節,ZI 節的資料都被初始化為 0。加載完後 RAM 區準備完畢,正式開始執行主體程式。
編譯生成的 RW-data 的資料屬于圖中的 RW 節,ZI-data 的資料屬于圖中的 ZI 節。是否需要掉電儲存,這就是把 RW-data 與 ZI-data 差別開來的原因,因為在RAM 建立資料的時候,預設值為 0,但如果有的資料要求初值非 0,那就需要使用 ROM 記錄該初始值,運作時再複制到 RAM。
STM32 的 RO 區域不需要加載到 SRAM,核心直接從 FLASH 讀取指令運作。計算機系統的應用程式運作過程很類似,不過計算機系統的程式在存儲狀态時位于硬碟,執行的時候甚至會把上述的 RO 區域(代碼、隻讀資料)加載到記憶體,加快運作速度,還有虛拟記憶體管理單元 (MMU) 輔助加載資料,使得可以運作比實體記憶體還大的應用程式。而 STM32 沒有 MMU,是以無法支援 Linux 和 Windows 系統。
當程式存儲到 STM32 晶片的内部 FLASH 時(即 ROM 區),它占用的空間是 Code、RO-data 及 RW-data 的總和,是以如果這些内容比 STM32 晶片的 FLASH 空間大,程式就無法被正常儲存了。當程式在執行的時候,需要占用内部 SRAM 空間(即 RAM 區),占用的空間包括 RW-data 和 ZI-data。應用程式在各個狀态時各區域的組成見下表。
在 MDK 中,我們建立的工程一般會選擇晶片型号,選擇後就有确定的 FLASH 及 SRAM 大小,若代碼超出了晶片的存儲器的極限,編譯器會提示錯誤,這時就需要裁剪程式了,裁剪時可針對超出的區域來優化。
4. 編譯工具鍊
在前面編譯過程中,MDK 調用了各種編譯工具,平時我們直接配置 MDK,不需要學習如何使用它們,但了解它們是非常有好處的。例如,若希望使用 MDK 編譯生成 bin 檔案的,需要在 MDK 中輸入指令控制 fromelf 工具;在本章後面講解 AXF 及 .O 檔案的時候,需要利用 fromelf 工具檢視其檔案資訊,這都是無法直接通過 MDK 做到的。關于這些工具鍊的說明,在 MDK 的幫助手冊《ARM Development Tools》都有詳細講解,點選 MDK 界面的 “help->uVision Help” 菜單可打開該檔案。
4.1 設定環境變量
調用這些編譯工具,需要用到 Windows 的"指令行提示符工具",為了讓指令行友善地找到這些工具,我們先把工具鍊的目錄添加到系統的環境變量中。檢視本機工具鍊所在的具體目錄可根據上一小節講解的工程編譯提示輸出資訊中找到,如本機的路徑為 “D:\work\keil5\ARM\ARMCC\bin”。
4.1.1 添加路徑到 PATH 環境變量
本文以 Win7 系統為例添加工具鍊的路徑到 PATH 環境變量,其它系統是類似的。
(1) 右鍵電腦系統的"計算機圖示",在彈出的菜單中選擇"屬性",見下圖:
(2) 在彈出的屬性頁面依次點選"進階系統設定 “->” 環境變量",在使用者變量一欄中找到名為 “PATH” 的變量,若沒有該變量,則建立一個。編輯 “PATH” 變量,在它的變量值中輸入工具鍊的路徑,如本機的是 “;D:\work\keil5\ARM\ARMCC\bin”,注意要使用"分号 “;” 讓它與其它路徑分隔開,輸入完畢後依次點确定,見下圖 ;
(3) 打開 Windows 的指令行,點選系統的"開始菜單",在搜尋框輸入 “cmd”,在搜尋結果中點選 “cmd.exe” 即可打開指令行,見下圖;
(4) 在彈出的指令行視窗中輸入 “fromelf” 回車,若視窗列印出 formelf 的幫助說明,那麼路徑正常,就可以開始後面的工作了;若提示"不是内部名外部指令,也不是可運作的程式…"資訊,說明路徑不對,請重新配置環境變量,并确認該工作目錄下有編譯工具鍊。
這個過程本質就是讓指令行通過 “PATH” 路徑找到 “fromelf.exe” 程式運作,預設運作 “fromelf.exe” 時它會輸出自己的幫助資訊,這就是工具鍊的調用過程,MDK 本質上也是如此調用工具鍊的,隻是它內建為 GUI,相對于指令行對使用者更友好,畢竟上述配置環境變量的過程已經讓新手煩躁了。
4.2 armcc、armasm及armlink
接下來我們看看各個工具鍊的具體用法,主要以 armcc 為例。
(1)armcc
armcc 用于把 c/c++ 檔案編譯成 ARM 指令代碼,編譯後會輸出 ELF 格式的 .O 檔案(對象、目标檔案),在指令行中輸入 “armcc” 回車可調用該工具,它會列印幫助說明,見下圖。
幫助提示中分三部分,第一部分是 armcc 版本資訊,第二部分是指令的用法,第三部分是主要指令選項。
根據指令用法: armcc [options] file1 file2 … filen ,在 [option] 位置可輸入下面的 “–arm”、"–cpu list" 選項,若選項帶檔案輸入,則把檔案名填充在 file1 file2…的位置,這些檔案一般是 c/c++ 檔案。
根據它的幫助說明,"–cpu list" 可列出編譯器支援的所有 cpu,我們在指令行中輸入 “armcc --cpu list”,可檢視下圖中的cpu清單。
打開 MDK 的 Options for Targe->c/c++ 菜單,可看到 MDK 對編譯器的控制指令,見下圖。
從該圖中的指令可看到,它調用了 -c、-cpu –D –g –O0 等編譯選項,當我們修改 MDK 的編譯配置時,可看到該控制指令也會有相應的變化。然而我們無法在該編譯選項框中輸入指令,隻能通過 MDK 提供的選項修改。
了解這些,我們就可以查詢具體的 MDK 編譯選項的具體資訊了,如 c/c++ 選項中的 " Optimization:Leve 1(-O1)" 是什麼功能呢?首先可了解到它是 “-O” 指令,指令後還帶個數字,檢視 MDK 的幫助手冊,在 armcc 編譯器說明章節,可詳細了解,如下圖。
利用 MDK,我們一般不需要自己調用 armcc 工具,但經過這樣的過程我們就會對 MDK 有更深入的認識。
(2)armasm
armasm 是彙編器,它把彙編檔案編譯成 .O 檔案。與 armcc 類似,MDK 對 armasm 的調用選項可在 “Option for Target->Asm” 頁面進行配置,見下圖。
(3)armlink
armlink 是連結器,它把各個 .O 檔案連結組合在一起生成 ELF 格式的 AXF 檔案,AXF 檔案是可執行的,下載下傳器把該檔案中的指令代碼下載下傳到晶片後,該晶片就能運作程式了;利用 armlink 還可以控制程式存儲到指定的 ROM 或 RAM 位址。在 MDK 中可在 “Option for Target->Linker” 頁面配置 armlink 選項,見下圖 。
連結器預設是根據晶片類型的存儲器分布來生成程式的,該存儲器分布被記錄在工程裡的 sct 字尾的檔案中,有特殊需要的話可自行編輯該檔案,改變連結器的連結方式。
4.3 armar、fromelf及使用者指令
armar 工具用于把工程打包成庫檔案,fromelf 可根據 axf 檔案生成 hex、bin 檔案,hex 和 bin 檔案是大多數下載下傳器支援的下載下傳檔案格式。
在 MDK 中,針對 armar 和 fromelf 工具的選項幾乎沒有,僅內建了生成 HEX 或 Lib 的選項,見下圖。
例如如果我們想利用 fromelf 生成 bin 檔案,可以在 MDK 的 “Option for Target->User” 頁中添加調用 fromelf 的指令,見下圖。
在 User 配置頁面中,提供了三種類型的使用者指令輸入框,在不同組的框輸入指令,可控制指令的執行時間,分别是編譯前 (Before Compile c/c++ file)、建構前 (Before Build/Rebuild) 及建構後 (After Build/Rebuild) 執行。這些指令并沒有限制必須是 arm 的編譯工具鍊,例如如果您自己編寫了 python 腳本,也可以在這裡輸入使用者指令執行該腳本。
圖中的生成 bin 檔案指令調用了 fromelf 工具,緊跟後面的是工具的選項及輸出檔案名、輸入檔案名。由于 fromelf 是根據 axf 檔案生成 bin 的,而 axf 檔案又是建構 (build) 工程後才生成,是以我們把該指令放到 “After Build/Rebuild” 一欄。
5.MDK工程的檔案類型
除了上述編譯過程生成的檔案,MDK 工程中還包含了各種各樣的檔案,下面我們統一介紹,MDK 工程的常見檔案類型見下表。
這些檔案主要分為MDK相關檔案、源檔案以及編譯、連結器生成的檔案。我們以"多彩流水燈"工程為例講解各種檔案的功能。(參看:MDK的編譯過程及檔案類型全解——(二))
轉載自——第48章 MDK的編譯過程及檔案類型全解