MDK 的編譯過程及文件類型全解

出處:MDK 的編譯過程及文件類型全解

 

MDK 的編譯過程及文件類型全解

------(在arm9的開發中,這些東西都是我們自己搞定的,但是在windows上,IDE幫我們做好了,瞭解這些對深入開發是很有幫助的,在有arm9開發的基礎上,下面的東西很容易理解,如果看不懂,證明你還沒有入門。下面的是從world複製過來的,格式和博客不太兼容,所有開始以字母q的,是world中的 □ 字符)

本章參考資料: MDK 的幫助手冊《ARM Development Tools》,點擊 MDK 界面的
“help->uVision Help”菜單可打開該文件。關於 ELF 文件格式,參考配套資料裏的《ELF
文件格式》文件。
在本章中講解了非常多的文件類型,學習時請跟着教程的節奏,打開實際工程中的文
件來了解。
相信您已經非常熟練地使用 MDK 創建應用程序了,平時使用 MDK 編寫源代碼,然
後編譯生成機器碼,再把機器碼下載到 STM32 芯片上運行,但是這個編譯、下載的過程
MDK 究竟做了什麼工作?它編譯後生成的各種文件又有什麼作用?本章節將對這些過程進
行講解,瞭解編譯及下載過程有助於理解芯片的工作原理,這些知識對製作 IAP(bootloader)
以及讀寫控制器內部 FLASH 的應用時非常重要。

 

 

編譯過程生成的不同文件將在後面的小節詳細說明,此處先抓住主要流程來理解。
(1) 編譯, MDK 軟件使用的編譯器是 armcc 和 armasm,它們根據每個 c/c++和彙編源文件
編譯成對應的以“.o”爲後綴名的對象文件(Object Code,也稱目標文件),其內容主要
是從源文件編譯得到的機器碼,包含了代碼、數據以及調試使用的信息;
(2) 鏈接,鏈接器 armlink 把各個.o 文件及庫文件鏈接成一個映像文件“.axf”或“.elf”;

(3) 格式轉換,一般來說 Windows 或 Linux 系統使用鏈接器直接生成可執行映像文件 elf
後,內核根據該文件的信息加載後,就可以運行程序了,但在單片機平臺上,需要把
該文件的內容加載到芯片上,所以還需要對鏈接器生成的 elf 映像文件利用格式轉換器
fromelf 轉換成“.bin”或“.hex”文件,交給下載器下載到芯片的 FLASH 或 ROM 中。

具體工程中的編譯過程
下面我們打開 “多彩流水燈”的工程,以它爲例進行講解,其它工程的編譯過程也是
一樣的,只是文件有差異。打開工程後,點擊 MDK 的“rebuild”按鈕,它會重新構建整
個工程,構建的過程會在 MDK 下方的“Build Output”窗口輸出提示信息,見圖 48-2。

 

 

(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”目錄下找到由以上過程生成的各種
文件,見圖 48-4。

 

 

可以看到,每個 C 源文件都對應生成了.o、 .d 及.crf 後綴的文件,還有一些額外
的.dep、 .hex、 .axf、 .htm、 .lnp、 .sct、 .lst 及.map 文件。

程序的組成、存儲與運行
CODE、 RO、 RW、 ZI Data 域及堆棧空間
在工程的編譯提示輸出信息中有一個語句“Program Size: Code=xx RO-data=xx RWdata=xx ZI-data=xx”,它說明了程序各個域的大小,編譯後,應用程序中所有具有同一性質的數據(包括代碼)被歸到一個域,程序在存儲或運行的時候,不同的域會呈現不同的狀
態,這些域的意義如下:
q Code:即代碼域,它指的是編譯器生成的機器指令,這些內容被存儲到 ROM 區。
q RO-data: Read Only data,即只讀數據域,它指程序中用到的只讀數據,這些數
據被存儲在 ROM 區,因而程序不能修改其內容。例如 C 語言中 const 關鍵字定義
的變量就是典型的 RO-data(注:C語言中,const修飾的變量還是可以通過指針更改,c++中是不允許的)。
q RW-data: Read Write data,即可讀寫數據域,它指初始化爲“非 0 值”的可讀寫
數據,程序剛運行時,這些數據具有非 0 的初始值,且運行的時候它們會常駐在
RAM 區,因而應用程序可以修改其內容。例如 C 語言中使用定義的全局變量,
且定義時賦予“非 0 值”給該變量進行初始化。
q ZI-data: Zero Initialie data,即 0 初始化數據,它指初始化爲“0 值”的可讀寫數
據域,它與 RW-data 的區別是程序剛運行時這些數據初始值全都爲 0,而後續運
行過程與 RW-data 的性質一樣,它們也常駐在 RAM 區,因而應用程序可以更改
其內容。例如 C 語言中使用定義的全局變量,且定義時賦予“0 值”給該變量進
行初始化(若定義該變量時沒有賦予初始值,編譯器會把它當 ZI-data 來對待,初
始化爲 0);
q ZI-data 的棧空間(Stack)及堆空間(Heap):在 C 語言中,函數內部定義的局部變量
屬於棧空間,進入函數的時候從向棧空間申請內存給局部變量,退出時釋放局部
變量,歸還內存空間。而使用 malloc 動態分配的變量屬於堆空間。在程序中的棧
空間和堆空間都是屬於 ZI-data 區域的,這些空間都會被初始值化爲 0 值。編譯器
給出的 ZI-data 佔用的空間值中包含了堆棧的大小(經實際測試,若程序中完全沒
有使用 malloc 動態申請堆空間,編譯器會優化,不把堆空間計算在內)。

程序的存儲與運行
RW-data 和 ZI-data 它們僅僅是初始值不一樣而已,爲什麼編譯器非要把它們區分開?
這就涉及到程序的存儲狀態了,應用程序具有靜止狀態和運行狀態。靜止態的程序被存儲
在非易失存儲器中,如 STM32 的內部 FLASH,因而系統掉電後也能正常保存。但是當程
序在運行狀態的時候,程序常常需要修改一些暫存數據,由於運行速度的要求,這些數據
往往存放在內存中(RAM),掉電後這些數據會丟失。因此,程序在靜止與運行的時候它在
存儲器中的表現是不一樣的,見圖 48-5。

 

 

圖中的左側是應用程序的存儲狀態,右側是運行狀態,而上方是 RAM 存儲器區域,
下方是 ROM 存儲器區域。
程序在存儲狀態時, 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 系統。

 

 

編譯工具鏈
在前面編譯過程中, MDK 調用了各種編譯工具,平時我們直接配置 MDK,不需要學
習如何使用它們,但瞭解它們是非常有好處的。例如,若希望使用 MDK 編譯生成 bin 文件
的,需要在 MDK 中輸入指令控制 fromelf 工具;在本章後面講解 AXF 及 O 文件的時候,
需要利用 fromelf 工具查看其文件信息,這都是無法直接通過 MDK 做到的。關於這些工具
鏈的說明,在 MDK 的幫助手冊《ARM Development Tools》都有詳細講解,點擊 MDK 界
面的“help->uVision Help”菜單可打開該文件。

設置環境變量
調用這些編譯工具,需要用到 Windows 的“命令行提示符工具”,爲了讓命令行方便
地找到這些工具,我們先把工具鏈的目錄添加到系統的環境變量中。查看本機工具鏈所在
的具體目錄可根據上一小節講解的工程編譯提示輸出信息中找到,如本機的路徑爲
“D:\work\keil5\ARM\ARMCC\bin”。
1. 添加路徑到 PATH 環境變量
本文以 Win7 系統爲例添加工具鏈的路徑到 PATH 環境變量,其它系統是類似的。
(1) 右鍵電腦系統的“計算機圖標”,在彈出的菜單中選擇“屬性”,見圖 48-6;

 

 

(2) 在彈出的屬性頁面依次點擊“高級系統設置” ->“環境變量”,在用戶變量一欄
中找到名爲“PATH”的變量,若沒有該變量,則新建一個。編輯“PATH”變量,
在它的變量值中輸入工具鏈的路徑,如本機的是
“;D:\work\keil5\ARM\ARMCC\bin”,注意要使用“分號;”讓它與其它路徑分隔
開,輸入完畢後依次點確定,見圖 48-7;

 

 

(3) 打開 Windows 的命令行,點擊系統的“開始菜單”,在搜索框輸入
“cmd”,在搜索結果中點擊“cmd.exe”即可打開命令行,見圖 48-8;

 

(4) 在彈出的命令行窗口中輸入“fromelf”回車,若窗口打印出 formelf 的幫助說明,
那麼路徑正常,就可以開始後面的工作了;若提示“不是內部名外部命令,也不
是可運行的程序…”信息, 說明路徑不對,請重新配置環境變量,並確認該工作
目錄下有編譯工具鏈。
這個過程本質就是讓命令行通過“PATH”路徑找到“fromelf.exe”程序運行,默認運
行“fromelf.exe”時它會輸出自己的幫助信息,這就是工具鏈的調用過程, MDK 本質上也
是如此調用工具鏈的,只是它集成爲 GUI,相對於命令行對用戶更友好,畢竟上述配置環
境變量的過程已經讓新手煩躁了。

armcc、 armasm 及 armlink
略,在linux arm開發中已經熟悉過了。

補充MDK生成bin文件:

只能通過命令行:

 

命令行:#K\ARM\ARMCC\bin\fromelf.exe --bin -o #L.bin #L  (#L.bin可以改成@L.bin,主要是生成的路徑不同,@生成在工程文件文件夾,#生成在輸出文件文件夾裏)

這樣就可以生成bin文件了。

MDK 工程的文件類型
除了上述編譯過程生成的文件, MDK 工程中還包含了各種各樣的文件,下面我們統一
介紹, MDK 工程的常見文件類型見表 48-3。

 

 

uvprojx、 uvoptx、 uvguix 及 ini 工程文件
在工程的“Project”目錄下主要是 MDK 工程相關的文件,見圖 48-17。

 

 

  1. uvprojx 文件
    uvprojx 文件就是我們平時雙擊打開的工程文件,它記錄了整個工程的結構,如芯片類
    型、工程包含了哪些源文件等內容,見圖 48-18。

 

 

  1. uvoptx 文件
    uvoptx 文件記錄了工程的配置選項,如下載器的類型、變量跟蹤配置、斷點位置以及
    當前已打開的文件等等,見圖 48-19。

 

 

  1. uvprojx 文件
    uvguix 文件記錄了 MDK 軟件的 GUI 佈局,如代碼編輯區窗口的大小、編譯輸出提示
    窗口的位置等等。

 

 

uvprojx、 uvoptx 及 uvguix 都是使用 XML 格式記錄的文件,若使用記事本打開可以看
到 XML 代碼,見圖 48-17。而當使用 MDK 軟件打開時,它根據這些文件的 XML 記錄加
載工程的各種參數,使得我們每次重新打開工程時,都能恢復上一次的工作環境。

 

 

這些工程參數都是當 MDK 正常退出時纔會被寫入保存,所以若 MDK 錯誤退出時(如
使用 Windows 的任務管理器強制關閉),工程配置參數的最新更改是不會被記錄的,重新
打開工程時要再次配置。根據這幾個文件的記錄類型,可以知道 uvprojx 文件是最重要的,
刪掉它我們就無法再正常打開工程了,而 uvoptx 及 uvguix 文件並不是必須的,可以刪除,重新使用 MDK 打開 uvprojx 工程文件後,會以默認參數重新創建 uvoptx 及 uvguix 文件。
(所以當使用 Git/SVN 等代碼管理的時候,往往只保留 uvprojx 文件)

Output 目錄下生成的文件
點擊 MDK 中的編譯按鈕,它會根據工程的配置及工程中的源文件輸出各種對象和列
表文件,在工程的“Options for Targe->Output->Select Folder for Objects”和“Options for
Targe->Listing->Select Folder for Listings”選項配置它們的輸出路徑,見圖 48-22 和圖 48-23。

 

 

  1. lib 庫文件
    在某些場合下我們需要提供給第三方一個可用的代碼庫,但不希望對方看到源碼,這個時候我們就可以把工程生成 lib 文件(Library file)提供給對方,在 MDK 中可配置“Options for Target->Create Library”選項把工程編譯成庫文件,見圖 48-25。

 

 

工程中生成可執行文件或庫文件只能二選一,默認編譯是生成可執行文件的,可執行
文件即我們下載到芯片上直接運行的機器碼。
得到生成的*.lib 文件後,可把它像 C 文件一樣添加到其它工程中,並在該工程調用 lib提供的函數接口,除了不能看到*.lib 文件的源碼,在應用方面它跟 C 源文件沒有區別。

  1. dep、 d 依賴文件
    *.dep 和*.d 文件(Dependency file)記錄的是工程或其它文件的依賴, 主要記錄了引用的頭文件路徑, 其中*.dep 是整個工程的依賴, 它以工程名命名, 而*.d 是單個源文件的依賴,它們以對應的源文件名命名。這些記錄使用文本格式存儲,我們可直接使用記事本打開,見圖 48-26 和圖 48-27。

 

 

 

  1. crf 交叉引用文件
    *.crf 是交叉引用文件(Cross-Reference file),它主要包含了瀏覽信息(browse information),即源代碼中的宏定義、變量及函數的定義和聲明的位置。我們在代碼編輯器中點擊“Go To Definition Of ‘xxxx’”可實現瀏覽跳轉,見圖 48-28,跳轉的時候, MDK 就是通過*.crf 文件查找出跳轉位置的。

 

 

通過配置 MDK 中的“Option for Target->Output->Browse Information”選項可以設置編譯時是否生成瀏覽信息,見圖 48-29。只有勾選該選項並編譯後,才能實現上面的瀏覽跳轉功能。

 

  1. o、 axf 及 elf 文件
    *.o、 *.elf、 *.axf、 *.bin 及*.hex 文件都存儲了編譯器根據源代碼生成的機器碼,根據應用場合的不同,它們又有所區別。

ELF 文件說明
*.o、 *.elf、 *.axf 以及前面提到的 lib 文件都是屬於目標文件,它們都是使用 ELF 格式來存儲的,關於 ELF 格式的詳細內容請參考配套資料裏的《ELF 文件格式》文檔瞭解,它講解的是 Linux 下的 ELF 格式,與 MDK 使用的格式有小區別,但大致相同。在本教程中,僅講解 ELF 文件的核心概念。
ELF 是 Executable and Linking Format 的縮寫,譯爲可執行鏈接格式,該格式用於記錄目標文件的內容。在 Linux 及 Windows 系統下都有使用該格式的文件(或類似格式)用於記錄應用程序的內容,告訴操作系統如何鏈接、加載及執行該應用程序。

目標文件主要有如下三種類型:
(1) 可重定位的文件(Relocatable File), 包含基礎代碼和數據,但它的代碼及數據都沒有指定絕對地址,因此它適合於與其他目標文件鏈接來創建可執行文件或者共享
目標文件。 這種文件一般由編譯器根據源代碼生成。
例如 MDK 的 armcc 和 armasm 生成的*.o 文件就是這一類,另外還有 Linux
的*.o 文件, Windows 的 *.obj 文件。
(2) 可執行文件(Executable File) ,它包含適合於執行的程序, 它內部組織的代碼數據都有固定的地址(或相對於基地址的偏移),系統可根據這些地址信息把程序加載到內存執行。 這種文件一般由鏈接器根據可重定位文件鏈接而成,它主要是組織各個可重定位文件,給它們的代碼及數據一一打上地址標號,固定其在程序內部
的位置,鏈接後,程序內部各種代碼及數據段不可再重定位(即不能再參與鏈接器
的鏈接)。
例如 MDK 的 armlink 生成的*.elf 及*.axf 文件, (使用 gcc 編譯工具可生成
*.elf 文件,用 armlink 生成的是*.axf 文件, *.axf 文件在*.elf 之外,增加了調試使用的信息,其餘區別不大,後面我們僅講解*.axf 文件),另外還有 Linux 的/bin/bash 文件, Windows 的*.exe 文件。
(3) 共享目標文件(Shared Object File), 它的定義比較難理解,我們直接舉例, MDK
生成的*.lib 文件就屬於共享目標文件,它可以繼續參與鏈接,加入到可執行文件
之中。 另外, Linux 的.so,如/lib/ glibc-2.5.so, Windows 的 DLL 都屬於這一類。

  • o 文件與 axf 文件的關係
    根據上面的分類,我們瞭解到, *.axf 文件是由多個*.o 文件鏈接而成的,而*.o 文件由相應的源文件編譯而成,一個源文件對應一個*.o 文件。它們的關係見圖 48-31。

 

 

圖中的中間代表的是 armlink 鏈接器,在它的右側是輸入鏈接器的*.o 文件,左側是它輸出的*axf 文件。
可以看到,由於都使用 ELF 文件格式, *.o 與*.axf 文件的結構是類似的,它們包含ELF 文件頭、程序頭、節區(section)以及節區頭部表。各個部分的功能說明如下:
q ELF 文件頭用來描述整個文件的組織,例如數據的大小端格式,程序頭、節區頭
在文件中的位置等。
q 程序頭告訴系統如何加載程序,例如程序主體存儲在本文件的哪個位置,程序的大小,程序要加載到內存什麼地址等等。 MDK 的可重定位文件*.o 不包含這部分內容,因爲它還不是可執行文件,而 armlink 輸出的*.axf 文件就包含該內容了。
q 節區是*.o 文件的獨立數據區域,它包含提供給鏈接視圖使用的大量信息,如指令(Code)、數據(RO、 RW、 ZI-data)、符號表(函數、變量名等)、重定位信息等,例如每個由 C 語言定義的函數在*.o 文件中都會有一個獨立的節區;
q 存儲在最後的節區頭則包含了本文件節區的信息,如節區名稱、大小等等。
總的來說,鏈接器把各個*.o 文件的節區歸類、排列,根據目標器件的情況編排地址生成輸出,彙總到*.axf 文件。例如,見圖 48-32,“多彩流水燈”工程中在“bsp_led.c”文件中有一個 LED_GPIO_Config 函數,而它內部調用了“stm32f4xx_gpio.c”的 GPIO_Init 函數,經過 armcc 編譯後, LED_GPIO_Config 及 GPIO_Iint 函數都成了指令代碼,分別存儲在 bsp_led.o 及 stm32f4xx_gpio.o 文件中,這些指令在*.o 文件都沒有指定地址,僅包含了內容、大小以及調用的鏈接信息,而經過鏈接器後,鏈接器給它們都分配了特定的地址,並且把地址根據調用指向鏈接起來。

 

ELF 文件頭
接下來我們看看具體文件的內容,使用 fromelf 文件可以查看*.o、 *.axf 及*.lib 文件的
ELF 信息。
使用命令行,切換到文件所在的目錄,輸入“fromelf –text –v bsp_led.o”命令,可控
制輸出 bsp_led.o 的詳細信息,見圖 48-33。 利用“-c、 -z”等選項還可輸出反彙編指令文
件、代碼及數據文件等信息,請親手嘗試一下。

 

生成 bin 文件
使用 MDK 生成 bin 文件需要使用 fromelf 命令,在 MDK 的“Options For Target->Users”中加入圖 48-35 中的命令。

 

 

圖中的指令內容爲:
“fromelf --bin --output ..\..\Output\多彩流水燈.bin ..\..\Output\多彩流水燈.axf”
該指令是根據本機及工程的配置而寫的,在不同的系統環境或不同的工程中,指令內容都不一樣,我們需要理解它,才能爲自己的工程定製指令,首先看看 fromelf 的幫助,見圖 48-36。

 

 

我們在 MDK 輸入的指令格式是遵守 fromelf 幫助裏的指令格式說明的,其格式爲:
“fromelf [options] input_file”
其中 optinos 是指令選項,一個指令支持輸入多個選項,每個選項之間使用空格隔開,我們的實例中使用“--bin”選項設置輸出 bin 文件,使用“--output file”選項設置輸出文件的名字爲“..\..\Output\多彩流水燈.bin”,這個名字是一個相對路徑格式,如果不瞭解如何使用“..\”表示路徑,可使用 MDK 命令輸入框後面的文件夾圖標打開文件瀏覽器選擇文件,在命令的最後使用“..\..\Output\多彩流水燈.axf”作爲命令的輸入文件。具體的格式分解見圖 48-37。

 

 

fromelf 需要根據工程的*.axf 文件輸入來轉換得到 bin 文件,所以在命令的輸入文件參數中要選擇本工程對應的*.axf 文件,在 MDK 命令輸入欄中,我們把 fromelf 指令放置在“After Build/Rebuild” (工程構建完成後執行)一欄也是基於這個考慮,這樣設置後,工程構建完成生成了最新的*.axf 文件, MDK 再執行 fromelf 指令,從而得到最新的 bin 文件。設置完成生成 hex 的選項或添加了生成 bin 的用戶指令後,點擊工程的編譯(build)按鈕,重新編譯工程,成功後可看到圖 48-38 中的輸出。打開相應的目錄即可找到文件,若找不到 bin 文件,請查看提示輸出欄執行指令的信息,根據信息改正 fromelf 指令。

 

 

hex 文件格式
hex 是 Intel 公司制定的一種使用 ASCII 文本記錄機器碼或常量數據的文件格式,這種文件常常用來記錄將要存儲到 ROM 中的數據,絕大多數下載器支持該格式。
一個 hex 文件由多條記錄組成,而每條記錄由五個部分組成,格式形如
“:llaaaatt[dd…]cc”,例如本“多彩流水燈”工程生成的 hex 文件前幾條記錄見代碼清單48-9。

 

 

 

例如, 代碼清單 48-9 中的第一條記錄解釋如下:
(1) 02:表示這條記錄數據區的長度爲 2 字節;
(2) 0000:表示這條記錄要存儲到的地址;
(3) 04:表示這是一條擴展線性地址記錄;

(4) 0800:由於這是一條擴展線性地址記錄,所以這部分表示地址的高 16 位,與前面的“0000”結合在一起,表示要擴展的線性地址爲“0x0800 0000”,這正好是
STM32 內部 FLASH 的首地址;
(5) F2:表示校驗和,它的值爲(0x02+0x00+0x00+0x04+0x08+0x00)%256 的值再取補碼。
再來看第二條記錄:
(1) 10:表示這條記錄數據區的長度爲 2 字節;
(2) 0000:表示這條記錄所在的地址,與前面的擴展記錄結合,表示這條記錄要存儲的 FLASH 首地址爲(0x0800 0000+0x0000);
(3) 00:表示這是一條數據記錄,數據區的是地址;
(4) 00040020C10100081B030008A3020008:這是要按地址存儲的數據;
(5) 2F:校驗和爲了更清楚地對比 bin、 hex 及 axf 文件的差異,我們來查看這些文件內部記錄的信息來進行對比。

 

 

如果您想要親自閱讀自己電腦上的 bin 文件,推薦使用sublime 軟件打開,它可以把二進制數以 ASCII 碼呈現出來,便於閱讀。

htm 靜態調用圖文件
在 Output 目錄下,有一個以工程文件命名的後綴爲*.bulid_log.htm 及*.htm 文件,如“多彩流水燈.bulid_log.htm”及“多彩流水燈.htm”,它們都可以使用瀏覽器打開。其中*.build_log.htm 是工程的構建過程日誌,而*.htm 是鏈接器生成的靜態調用圖文件。
在靜態調用圖文件中包含了整個工程各種函數之間互相調用的關係圖,而且它還給出了靜態佔用最深的棧空間數量以及它對應的調用關係鏈。
例如圖 48-43 是“多彩流水燈.htm”文件頂部的說明。

 

 

該文件說明了本工程的靜態棧空間最大佔用 56 字節(Maximum Stack Usage:56bytes),這個佔用最深的靜態調用爲“main->LED_GPIO_Config->GPIO_Init”。注意這裏給出的空間只是靜態的棧使用統計,鏈接器無法統計動態使用情況,例如鏈接器無法知道遞歸函數的遞歸深度。在本文件的後面還可查詢到其它函數的調用情況及其它細節。利用這些信息,我們可以大致瞭解工程中應該分配多少空間給棧,有空間餘量的情況下,一般會設置比這個靜態最深棧使用量大一倍,在 STM32 中可修改啓動文件改變堆棧的大小;如果空間不足,可從本文件中瞭解到調用深度的信息,然後優化該代碼。
注意:
查看了各個工程的靜態調用圖文件統計後,我們發現本書提供的一些比較大規模的工程例子,靜態棧調用最大深度都已超出 STM32 啓動文件默認的棧空間大小 0x00000400,即 1024 字節,但在當時的調試過程中卻沒有發現錯誤,因此我們也沒有修改棧的默認大小(有一些工程調試時已發現問題,它們的棧空間就已經被我們改大了),雖然這些工程實際運行並沒有錯誤,但這可能只是因爲它使用的棧溢出 RAM 空間恰好沒被程序其它部分修改而已。所以,建議您在實際的大型工程應用中(特別是使用了各種外部庫時,如Lwip/emWin/Fatfs 等),要查看本靜態調用圖文件,瞭解程序的棧使用情況,給程序分配合適的棧空間。

Listing 目錄下的文件
在 Listing 目錄下包含了*.map 及*.lst 文件,它們都是文本格式的,可使用 Windows 的記事本軟件打開。其中 lst 文件僅包含了一些彙編符號的鏈接信息,我們重點分析 map 文件。

  1. map 文件說明
    map 文件是由鏈接器生成的,它主要包含交叉鏈接信息,查看該文件可以瞭解工程中各種符號之間的引用以及整個工程的 Code、 RO-data、 RW-data 以及 ZI-data 的詳細及彙總信息。它的內容中主要包含了“節區的跨文件引用”、“刪除無用節區”、“符號映像表”、“存儲器映像索引”以及“映像組件大小”,各部分介紹如下:

 

在這部分中,詳細列出了各個*.o 文件之間的符號引用。由於*.o 文件是由 asm 或 c/c++源文件編譯後生成的,各個文件及文件內的節區間互相獨立,鏈接器根據它們之間的互相引用鏈接起來,鏈接的詳細信息在這個“Section Cross References”一一列出。例如,開頭部分說明的是 startup_stm32f429_439xx.o 文件中的“RESET”節區分爲它使用的“__initial_sp” 符號引用了同文件“STACK”節區。也許我們對啓動文件不熟悉,不清楚這究竟是什麼,那我們繼續瀏覽,可看到 main.o文件的引用說明,如說明 main.o 文件的 i.main 節區爲它使用的 LED_GPIO_Config 符號引用了 bsp_led.o 文件的 i.LED_GPIO_Config 節區。同樣地,下面還有 bsp_led.o 文件的引用說明,如說明了 bsp_led.o 文件的i.LED_GPIO_Config 節區爲它使用的 GPIO_Init 符號引用了 stm32f4xx_gpio.o 文件的i.GPIO_Init 節區。
可以瞭解到,這些跨文件引用的符號其實就是源文件中的函數名、變量名。有時在構
建工程的時候,編譯器會輸出 “Undefined symbol xxx (referred from xxx.o)” 這樣的提示,該提示的原因就是在鏈接過程中,某個文件無法在外部找到它引用的標號,因而產生鏈接錯誤。例如,見圖 48-44,我們把 bsp_led.c 文件中定義的函數 LED_GPIO_Config 改名爲LED_GPIO_ConfigABCD,而不修改 main.c 文件中的調用,就會出現 main 文件無法找到LED_GPIO_Config 符號的提示。

刪除無用節區
map 文件的第二部分是刪除無用節區的說明(Removing Unused input sections from theimage.),見代碼清單 48-11。

 

 

這部分列出了在鏈接過程它發現工程中未被引用的節區,這些未被引用的節區將會被
刪除(指不加入到*.axf 文件,不是指在*.o 文件刪除),這樣可以防止這些無用數據佔用程序空間。

例如,上面的信息中說明 startup_stm32f429_439xx.o 中的 HEAP(在啓動文件中定義的用於動態分配的“堆”區)以及 stm32f4xx_adc.o 的各個節區都被刪除了,因爲在我們這個工程中沒有使用動態內存分配,也沒有引用任何 stm32f4xx_adc.c 中的內容。由此也可以知道,雖然我們把 STM32 標準庫的各個外設對應的 c 庫文件都添加到了工程,但不必擔心這會使工程變得臃腫,因爲未被引用的節區內容不會被加入到最終的機器碼文件中。

符號映像表
map 文件的第三部分是符號映像表(Image Symbol Table), 見代碼清單 48-12。

 

 

這個表列出了被引用的各個符號在存儲器中的具體地址、佔據的空間大小等信息。如
我們可以查到 LED_GPIO_Config 符號存儲在 0x080002a5 地址,它屬於 Thumb Code 類型,大小爲 106 字節,它所在的節區爲 bsp_led.o 文件的 i.LED_GPIO_Config 節區。
存儲器映像索引
map 文件的第四部分是存儲器映像索引(Memory Map of the image), 見代碼清單 48-13。

 

本工程的存儲器映像索引分爲 ER_IROM1 及 RW_IRAM1 部分,它們分別對應 STM32
內部 FLASH 及 SRAM 的空間。相對於符號映像表,這個索引表描述的單位是節區,而且
它描述的主要信息中包含了節區的類型及屬性,由此可以區分 Code、 RO-data、 RW-data
及 ZI-data。
例如,從上面的表中我們可以看到 i.LED_GPIO_Config 節區存儲在內部 FLASH 的
0x080002a4 地址,大小爲 0x00000074,類型爲 Code,屬性爲 RO。而程序的 STACK 節區(棧空間)存儲在 SRAM 的 0x20000000 地址,大小爲 0x00000400,類型爲 Zero,屬性爲RW(即 RW-data) 。

映像組件大小

map 文件的最後一部分是包含映像組件大小的信息(Image component sizes),這也是最常
查詢的內容,見代碼清單 48-14。

 

這部分包含了各個使用到的*.o 文件的空間彙總信息、整個工程的空間彙總信息以及佔
用不同類型存儲器的空間彙總信息,它們分類描述了具體佔據的 Code、 RO-data、 RW-data及 ZI-data 的大小,並根據這些大小統計出佔據的 ROM 總空間。
我們僅分析最後兩部分信息,如 Grand Totals 一項,它表示整個代碼佔據的所有空間
信息,其中 Code 類型的數據大小爲 1012 字節,這部分包含了 84 字節的指令數據(inc .data)已算在內,另外 RO-data 佔 444 字節, RW-data 佔 0 字節, ZI-data 佔 1024 字節。在它的下面兩行有一項 ROM Totals 信息,它列出了各個段所佔據的 ROM 空間,除了 ZI-data 不佔ROM 空間外,其餘項都與 Grand Totals 中相等(RW-data 也佔據 ROM 空間,只是本工程中沒有 RW-data 類型的數據而已)。
最後一部分列出了只讀數據(RO)、可讀寫數據(RW)及佔據的 ROM 大小。其中只讀數
據大小爲 1456 字節, 它包含 Code 段及 RO-data 段; 可讀寫數據大小爲 1024 字節,它包含RW-data 及 ZI-data 段;佔據的 ROM 大小爲 1456 字節,它除了 Code 段和 RO-data 段,還包含了運行時需要從 ROM 加載到 RAM 的 RW-data 數據。
綜合整個 map 文件的信息,可以分析出,當程序下載到 STM32 的內部 FLASH 時,需
要使用的內部 FLASH 是從 0x0800 0000 地址開始的大小爲 1456 字節的空間;當程序運行時,需要使用的內部 SRAM 是從 0x20000000 地址開始的大小爲 1024 字節的空間。
粗略一看, 發現這個小程序竟然需要 1024 字節的 SRAM,實在說不過去,但仔細分析
map 文件後,可瞭解到這 1024 字節都是 STACK 節區的空間(即棧空間),棧空間大小是在
啓動文件中定義的,這 1024 字節是默認值(0x00000400)。它是提供給 C 語言程序局部變量申請使用的空間,若我們確認自己的應用程序不需要這麼大的棧,完全可以修改啓動文件,
把它改小一點,查看前面講解的 htm 靜態調用圖文件可瞭解靜態的棧調用情況,可以用它
作爲參考。

sct 分散加載文件的格式與應用
1. sct 分散加載文件簡介

當工程按默認配置構建時, MDK 會根據我們選擇的芯片型號,獲知芯片的內部
FLASH 及內部 SRAM 存儲器概況,生成一個以工程名命名的後綴爲*.sct 的分散加載文件
(Linker Control File, scatter loading),鏈接器根據該文件的配置分配各個節區地址,生成分
散加載代碼,因此我們通過修改該文件可以定製具體節區的存儲位置。
例如可以設置源文件中定義的所有變量自動按地址分配到外部 SDRAM,這樣就不需
要再使用關鍵字“__attribute__”按具體地址來指定了;利用它還可以控制代碼的加載區與
執行區的位置,例如可以把程序代碼存儲到單位容量價格便宜的 NAND-FLASH 中,但在
NAND-FLASH 中的代碼是不能像內部 FLASH 的代碼那樣直接提供給內核運行的,這時可
通過修改分散加載文件,把代碼加載區設定爲 NAND-FLASH 的程序位置,而程序的執行
區設定爲 SDRAM 中的位置,這樣鏈接器就會生成一個配套的分散加載代碼,該代碼會把
NAND-FLASH 中的代碼加載到 SDRAM 中,內核再從 SDRAM 中運行主體代碼,大部分
運行 Linux 系統的代碼都是這樣加載的。

分散加載文件的格式
下面先來看看 MDK 默認使用的 sct 文件,在 Output 目錄下可找到“多彩流水燈.sct”,
該文件記錄的內容見代碼清單 48-15。

 

 

在默認的 sct 文件配置中僅分配了 Code、 RO-data、 RW-data 及 ZI-data 這些大區域的地址,鏈接時各個節區(函數、變量等)直接根據屬性排列到具體的地址空間。
sct 文件中主要包含描述加載域及執行域的部分,一個文件中可包含有多個加載域,而
一個加載域可由多個部分的執行域組成。同等級的域之間使用花括號“{}”分隔開,最外
層的是加載域,第二層“{}”內的是執行域,其整體結構見圖 48-45。

 

 

加載域
sct 文件的加載域格式見代碼清單 48-16。

 

 

q 加載域名:名稱,在 map 文件中的描述會使用該名稱來標識空間。如本例中只有
一個加載域,該域名爲 LR_IROM1。
q 基地址+地址偏移:這部分說明了本加載域的基地址,可以使用+號連接一個地址
偏移,算進基地址中,整個加載域以它們的結果爲基地址。如本例中的加載域基
地址爲 0x08000000,剛好是 STM32 內部 FLASH 的基地址。
q 屬性列表:屬性列表說明了加載域的是否爲絕對地址、 N 字節對齊等屬性,該配
置是可選的。本例中沒有描述加載域的屬性。
q 最大容量:最大容量說明了這個加載域可使用的最大空間,該配置也是可選的,
如果加上這個配置後,當鏈接器發現工程要分配到該區域的空間比容量還大,它
會在工程構建過程給出提示。本例中的加載域最大容量爲 0x00100000,即 1MB,正是本型號 STM32 內部 FLASH 的空間大小。

 

 

輸入節區描述
配合加載域及執行域的配置,在相應的域配置“輸入節區描述”即可控制該節區存儲
到域中,其格式見代碼清單 48-18。

 

 

q 模塊選擇樣式:模塊選擇樣式可用於選擇 o 及 lib 目標文件作爲輸入節區,它可以
直接使用目標文件名或“*”通配符,也可以使用“.ANY”。例如,使用語句“bsp_led.o”可以選擇 bsp_led.o 文件,使用語句“*.o”可以選擇所有 o 文件,使用“*.lib”可以選擇所有 lib 文件,使用“*”或“.ANY”可以選擇所有的 o 文件及 lib 文件。其中“.ANY”選擇語句的優先級是最低的,所有其它選擇語句選擇完剩下的數據纔會被“.ANY”語句選中。

q 輸入節區樣式:我們知道在目標文件中會包含多個節區或符號,通過輸入節區樣
式可以選擇要控制的節區。
示例文件中“(RESET, +First)”語句的 RESET 就是輸入節區樣式,它選擇
了名爲 RESET 的節區,並使用後面介紹的節區特性控制字“+First”表示它要存
儲到本區域的第一個地址。示例文件中的“*(InRoot$$Sections)”是一個鏈接器支
持的特殊選擇符號,它可以選擇所有標準庫裏要求存儲到 root 區域的節區,如
__main.o、 __scatter*.o 等內容。

q 輸入符號樣式:同樣地,使用輸入符號樣式可以選擇要控制的符號,符號樣式需
要使用“:gdef:”來修飾。例如可以使用“*(:gdef:Value_Test)”來控制選擇符號“Value_Test”。
q 輸入節區屬性:通過在模塊選擇樣式後面加入輸入節區屬性,可以選擇樣式中不
同的內容,每個節區屬性描述符前要寫一個“+”號,使用空格或“,”號分隔開,可以使用的節區屬性描述符見表 48-6。

  

例如,示例文件中使用“.ANY(+RO)”選擇剩餘所有節區 RO 屬性的內容都分配
到執行域 ER_IROM1 中,使用“.ANY(+RW +ZI)”選擇剩餘所有節區 RW 及 ZI 屬性
的內容都分配到執行域 RW_IRAM1 中。
q 節區特性:節區特性可以使用“+FIRST”或“+LAST”選項配置它要存儲到的位置,
FIRST 存儲到區域的頭部, LAST 存儲到尾部。通常重要的節區會放在頭部,而
CheckSum(校驗和)之類的數據會放在尾部。
例如示例文件中使用“(RESET,+First)”選擇了 RESET 節區,並要求把它放置到
本區域第一個位置,而 RESET 是工程啓動代碼中定義的向量表,見代碼清單 48-19,
該向量表中定義的堆棧頂和復位向量指針必須要存儲在內部 FLASH 的前兩個地址,
這樣 STM32 才能正常啓動,所以必須使用 FIRST 控制它們存儲到首地址。

 

 

 

總的來說,我們的 sct 示例文件配置如下:程序的加載域爲內部 FLASH 的 0x08000000,
最大空間爲 0x00100000;程序的執行基地址與加載基地址相同,其中 RESET 節區定義的
向量表要存儲在內部 FLASH 的首地址,且所有 o 文件及 lib 文件的 RO 屬性內容都存儲在內部 FLASH 中;程序執行時 RW 及 ZI 區域都存儲在以 0x20000000 爲基地址,大小爲
0x00030000 的空間(192KB),這部分正好是 STM32 內部主 SRAM 的大小。
鏈接器根據 sct 文件鏈接,鏈接後各個節區、符號的具體地址信息可以在 map 文件
中查看。

通過 MDK 配置選項來修改 sct 文件
瞭解 sct 文件的格式後,可以手動編輯該文件控制整個工程的分散加載配置,但 sct 文
件格式比較複雜,所以 MDK 提供了相應的配置選項可以方便地修改該文件,這些選項配
置能滿足基本的使用需求,本小節將對這些選項進行說明。
選擇 sct 文件的產生方式
首先需要選擇 sct 文件產生的方式,選擇使用 MDK 生成還是使用用戶自定義的 sct 文
件。在 MDK 的“Options for Target->Linker->Use Memory Layout from Target Dialog”選項
即可配置該選擇,見圖 48-46。

 

 

該選項的譯文爲“是否使用 Target 對話框中的存儲器分佈配置”,勾選後,它會根據
“Options for Target”對話框中的選項生成 sct 文件,這種情況下,即使我們手動打開它生
成的 sct 文件編輯也是無效的,因爲每次構建工程的時候, MDK 都會生成新的 sct 文件覆蓋舊文件。該選項在 MDK 中是默認勾選的,若希望 MDK 使用我們手動編輯的 sct 文件構建工程,需要取消勾選,並通過 Scatter File 框中指定 sct 文件的路徑,見圖 48-47。

 

 

通過 Target 對話框控制存儲器分配
若我們在 Linker 中勾選了“使用 Target 對話框的存儲器佈局”選項,那麼“Options
for Target”對話框中的存儲器配置就生效了。主要配置是在 Device 標籤頁中選擇芯片的類
型,設定芯片基本的內部存儲器信息以及在 Target 標籤頁中細化具體的存儲器配置(包括外部存儲器),見圖 48-48 及圖 48-49。

 

 

圖中 Device 標籤頁中選定了芯片的型號爲 STM32F429IGTx,選中後,在 Target 標籤
頁中的存儲器信息會根據芯片更新。

 

 

在 Target 標籤頁中存儲器信息分成只讀存儲器(Read/Only Memory Areas)和可讀寫存儲
器(Read/Write Memory Areas)兩類,即 ROM 和 RAM,而且它們又細分成了片外存儲器
(off-chip)和片內存儲器(on-chip)兩類。
例如,由於我們已經選定了芯片的型號, MDK 會自動根據芯片型號填充片內的 ROM
及 RAM 信息,其中的 IROM1 起始地址爲 0x80000000,大小爲 0x100000,正是該 STM32
型號的內部 FLASH 地址及大小;而 IRAM1 起始地址爲 0x20000000,大小爲 0x30000,正是該 STM32 內部主 SRAM 的地址及大小。圖中的 IROM1 及 IRAM1 前面都打上了勾,表示這個配置信息會被採用,若取消勾選,則該存儲配置信息是不會被使用的。
在標籤頁中的 IRAM2 一欄默認也填寫了配置信息,它的地址爲 0x10000000,大小爲0x10000,這是 STM32F4 系列特有的內部 64KB 高速 SRAM(被稱爲 CCM)。當我們希望使
用這部分存儲空間的時候需要勾選該配置,另外要注意這部分高速 SRAM 僅支持 CPU 總
線的訪問,不能通過外設訪問。
下面我們嘗試修改 Target 標籤頁中的這些存儲信息,例如,按照圖 48-50 中的 1 配置,
把 IRAM1 的基地址改爲 0x20001000,然後編譯工程,查看到工程的 sct 文件如代碼清單
48-20 所示;當按照圖 48-50 中的 2 配置時,同時使用 IRAM1 和 IRAM2,然後編譯工程,可查看到工程的 sct 文件如代碼清單 48-21 所示。

 

 

可以發現, sct 文件都根據 Target 標籤頁做出了相應的改變,除了這種修改外,在
Target 標籤頁上還控制同時使用 IRAM1 和 IRAM2、加入外部 RAM(如外接的 SDRAM),
外部 FLASH 等。
控制文件分配到指定的存儲空間
設定好存儲器的信息後,可以控制各個源文件定製到哪個部分存儲器,在 MDK 的工
程文件欄中,選中要配置的文件,右鍵,並在彈出的菜單中選擇“Options for File xxxx”
即可彈出一個文件配置對話框,在該對話框中進行存儲器定製,見圖 48-51。

 

 

在彈出的對話框中有一個“Memory Assignment”區域(存儲器分配),在該區域中可以
針對文件的各種屬性內容進行分配,如 Code/Const 內容(RO)、 Zero Initialized Data 內容(ZIdata)以及 Other Data 內容(RW-data),點擊下拉菜單可以找到在前面 Target 頁面配置的IROM1、 IRAM1、 IRAM2 等存儲器。例如圖中我們把這個 bsp_led.c 文件的 Other Data 屬性的內容分配到了 IRAM2 存儲器(在 Target 標籤頁中我們勾選了 IRAM1 及 IRAM2),當在bsp_led.c 文件定義了一些 RW-data 內容時(如初值非 0 的全局變量),該變量將會被分配到IRAM2 空間,配置完成後點擊 OK,然後編譯工程,查看到的 sct 文件內容見代碼清單48-22。

 

 

可以看到在 sct 文件中的 RW_IRAM2 執行域中增加了一個選擇 bsp_led.o 中 RW 內容
的語句。
類似地,我們還可以設置某些文件的代碼段被存儲到特定的 ROM 中,或者設置某些
文件使用的 ZI-data 或 RW-data 存儲到外部 SDRAM 中(控制 ZI-data 到 SDRAM 時注意還需要修改啓動文件設置堆棧對應的地址,原啓動文件中的地址是指向內部 SRAM 的)。
雖然 MDK 的這些存儲器配置選項很方便,但有很多高級的配置還是需要手動編寫 sct
文件實現的,例如 MDK 選項中的內部 ROM 選項最多只可以填充兩個選項位置,若想把內部 ROM 分成多片地址管理就無法實現了;另外 MDK 配置可控的最小粒度爲文件,若想控制特定的節區也需要直接編輯 sct 文件。
 

-------------——————內容若有錯誤,請您務必指出,感謝讓我提高並給予我建議的你---———————————---轉載請註明出處——————————---——------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章