天天看點

MDK的編譯過程及檔案類型全解——(二)

前言:

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

MDK的編譯過程及檔案類型全解——(二)

本文轉載自:第48章 MDK的編譯過程及檔案類型全解—零死角玩轉STM32-F429系列

1. MDK 相關檔案

1.1 uvprojx 檔案

uvprojx 檔案就是我們平時輕按兩下打開的工程檔案,它記錄了整個工程的結構,如晶片類型、工程包含了哪些源檔案等内容,見下圖 :

MDK的編譯過程及檔案類型全解——(二)

1.2 uvoptx 檔案

uvoptx 檔案記錄了工程的配置選項,如下載下傳器的類型、變量跟蹤配置、斷點位置以及目前已打開的檔案等等,見下圖:

MDK的編譯過程及檔案類型全解——(二)

1.3 uvguix 檔案

uvguix檔案記錄了 MDK 軟體的 GUI 布局,如代碼編輯區視窗的大小、編譯輸出提示視窗的位置等等。

MDK的編譯過程及檔案類型全解——(二)

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

MDK的編譯過程及檔案類型全解——(二)

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

2. 源檔案

源檔案是工程中我們最熟悉的内容了,它們就是我們編寫的各種源代碼,MDK 支援 c、cpp、h、s、inc 類型的源代碼檔案,其中 c、cpp 分别是 c/c++ 語言的源代碼,h 是它們的頭檔案,s 是彙編檔案,inc 是彙編檔案的頭檔案,可使用 “$include” 文法包含。編譯器根據工程中的源檔案最終生成機器碼。

3 . Output 目錄下生成的檔案

點選 MDK 中的編譯按鈕,它會根據工程的配置及工程中的源檔案輸出各種對象和清單檔案,在工程的 “Options for Targe->Output->Select Folder for Objects” 和 “Options for Targe->Listing->Select Folder for Listings” 選項配置它們的輸出路徑::Output 輸出路徑、Listing 輸出路徑。

MDK的編譯過程及檔案類型全解——(二)
MDK的編譯過程及檔案類型全解——(二)

編譯後 Output 和 Listing 目錄下生成的檔案見下圖:

MDK的編譯過程及檔案類型全解——(二)

3.1 lib 庫檔案

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

MDK的編譯過程及檔案類型全解——(二)

工程中生成可執行檔案或庫檔案隻能二選一,預設編譯是生成可執行檔案的,可執行檔案即我們下載下傳到晶片上直接運作的機器碼。

得到生成的 .lib 檔案後,可把它像 C 檔案一樣添加到其它工程中,并在該工程調用 lib 提供的函數接口,除了不能看到 .lib 檔案的源碼,在應用方面它跟 C 源檔案沒有差別。

3.2 dep、d 依賴檔案

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

MDK的編譯過程及檔案類型全解——(二)
MDK的編譯過程及檔案類型全解——(二)

3.3 crf 交叉引用檔案

.crf 是交叉引用檔案 (Cross-Reference file),它主要包含了浏覽資訊 (browse information),即源代碼中的宏定義、變量及函數的定義和聲明的位置。

我們在代碼編輯器中點選 “Go To Definition Of ‘xxxx’” 可實作浏覽跳轉,見下圖,跳轉的時候,MDK 就是通過 .crf 檔案查找出跳轉位置的。

MDK的編譯過程及檔案類型全解——(二)

通過配置 MDK 中的 “Option for Target->Output->Browse Information” 選項可以設定編譯時是否生成浏覽資訊,見下圖。隻有勾選該選項并編譯後,才能實作上面的浏覽跳轉功能。

MDK的編譯過程及檔案類型全解——(二)

.crf 檔案使用了特定的格式表示,直接用文本編輯器打開會看到大部分亂碼,見下圖,這裡不作深入研究。

MDK的編譯過程及檔案類型全解——(二)

3.4 o、axf及elf檔案

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

3.4.1 ELF 檔案說明

.o、.elf、.axf 以及前面提到的 lib 檔案都是屬于目标檔案,它們都是使用 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 都屬于這一類。

3.4.1.1 o檔案與axf檔案的關系

根據上面的分類,我們了解到, .axf 檔案是由多個 .o 檔案連結而成的,而 .o 檔案由相應的源檔案編譯而成,一個源檔案對應一個 .o 檔案。它們的關系見下圖:

MDK的編譯過程及檔案類型全解——(二)

上圖中的中間代表的是 armlink 連結器,在它的右側是輸傳入連結接器的 .o 檔案,左側是它輸出的 .axf 檔案。

可以看到,由于都使用 ELF 檔案格式, .o 與 .axf 檔案的結構是類似的,它們包含 ELF 檔案頭、程式頭、節區 (section) 以及節區頭部表。各個部分的功能說明如下:

(1)ELF 檔案頭用來描述整個檔案的組織,例如資料的大小端格式,程式頭、節區頭在檔案中的位置等。

(2)程式頭告訴系統如何加載程式,例如程式主體存儲在本檔案的哪個位置,程式的大小,程式要加載到記憶體什麼位址等等。MDK 的可重定位檔案 .o 不包含這部分内容,因為它還不是可執行檔案,而 armlink 輸出的 .axf 檔案就包含該内容了。

(3)節區是 .o 檔案的獨立資料區域,它包含提供給連結視圖使用的大量資訊,如指令 (Code)、資料 (RO、RW、ZI-data)、符号表(函數、變量名等)、重定位資訊等,例如每個由 C 語言定義的函數在 .o 檔案中都會有一個獨立的節區;

(4)存儲在最後的節區頭則包含了本檔案節區的資訊,如節區名稱、大小等等。

總的來說,連結器把各個 .o 檔案的節區歸類、排列,根據目标器件的情況編排位址生成輸出,彙總到 .axf 檔案。例如,見下圖,“多彩流水燈” 工程中在 “bsp_led.c” 檔案中有一個 LED_GPIO_Config 函數,而它内部調用了 “stm32f4xx_gpio.c” 的 GPIO_Init 函數,經過 armcc 編譯後,LED_GPIO_Config 及 GPIO_Iint 函數都成了指令代碼,分别存儲在 bsp_led.o 及 stm32f4xx_gpio.o 檔案中,這些指令在 .o 檔案都沒有指定位址,僅包含了内容、大小以及調用的連結資訊,而經過連結器後,連結器給它們都配置設定了特定的位址,并且把位址根據調用指向連結起來。

MDK的編譯過程及檔案類型全解——(二)
3.4.1.2 ELF 檔案頭

接下來我們看看具體檔案的内容,使用 fromelf 檔案可以檢視 .o、 .axf 及 .lib 檔案的 ELF 資訊。

使用指令行,切換到檔案所在的目錄,輸入 “fromelf –text –v bsp_led.o” 指令,可控制輸出 bsp_led.o 的詳細資訊,見下圖。利用 “-c、-z” 等選項還可輸出反彙編指令檔案、代碼及資料檔案等資訊,請親手嘗試一下。

MDK的編譯過程及檔案類型全解——(二)

為了便于閱讀,已使用 fromelf 指令生成了 “多彩流水燈.axf”、“bsp_led” 及"多彩流水燈 .lib" 的 ELF 資訊,并已把這些資訊儲存在獨立的檔案中,參見下表。

MDK的編譯過程及檔案類型全解——(二)
========================================================================
 ** ELF Header Information

 File Name:

 .\bsp_led.o         //bsp_led.o檔案

 Machine class: ELFCLASS32 (32-bit) //32位機

 Data encoding: ELFDATA2LSB (Little endian) //小端格式

 Header version: EV_CURRENT (Current version)

 Operating System ABI: none

 ABI Version: 0

 File Type: ET_REL (Relocatable object) (1) //可重定位檔案類型

 Machine: EM_ARM (ARM)

 Entry offset (in SHF_ENTRYSECT section): 0x00000000

 Flags: None (0x05000000)

 ARM ELF revision: 5 (ABI version 2)

 Built with

 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]

 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]

 Header size: 52 bytes (0x34)

 Program header entry size: 0 bytes (0x0)    //程式頭大小

 Section header entry size: 40 bytes (0x28)

 Program header entries: 0

 Section header entries: 246

 Program header offset: 0 (0x00000000) //程式頭在檔案中的位置(沒有程式頭)

 Section header offset: 507224 (0x0007bd58) //節區頭在檔案中的位置

 Section header string table index: 243


=====================================================================
           

在上述代碼中已加入了部分注釋,解釋了相應項的意義,值得一提的是在這個 .o 檔案中,它的 ELF 檔案頭中告訴我們它的程式頭 (Program header) 大小為 “0 bytes”,且程式頭所在的檔案位置偏移也為 “0”,這說明它是沒有程式頭的。

3.4.1.3 程式頭
===================================================================



 ** ELF Header Information



 File Name:
 .\多彩流水燈.axf //多彩流水燈.axf 檔案



 Machine class: ELFCLASS32 (32-bit) //32位機

 Data encoding: ELFDATA2LSB (Little endian) //小端格式

 Header version: EV_CURRENT (Current version)

 Operating System ABI: none

 ABI Version: 0

 File Type: ET_EXEC (Executable) (2) //可執行檔案類型

 Machine: EM_ARM (ARM)


 Image Entry point: 0x080001ad

 Flags: EF_ARM_HASENTRY + EF_ARM_ABI_FLOAT_SOFT (0x05000202)



 ARM ELF revision: 5 (ABI version 2)



 Conforms to Soft float procedure-call standard



 Built with

 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]

 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]



 Header size: 52 bytes (0x34)

 Program header entry size: 32 bytes (0x20)

 Section header entry size: 40 bytes (0x28)



 Program header entries: 1

 Section header entries: 15



 Program header offset: 335252 (0x00051d94) //程式頭在檔案中的位置

 Section header offset: 335284 (0x00051db4) //節區頭在檔案中的位置



 Section header string table index: 14



 =================================================================


 ** Program header #0



 Type : PT_LOAD (1) //表示這是可加載的内容

 File Offset : 52 (0x34) //在檔案中的偏移

 Virtual Addr : 0x08000000 //虛拟位址(此處等于實體位址)

 Physical Addr : 0x08000000 //實體位址

 Size in file : 1456 bytes (0x5b0) //程式在檔案中占據的大小

 Size in memory: 2480 bytes (0x9b0) //若程式加載到記憶體,占據的記憶體空間

 Flags : PF_X + PF_W + PF_R + PF_ARM_ENTRY (0x80000007)

 Alignment : 8 //位址對齊


===============================================================

           

對比之下,可發現 .axf 檔案的 ELF 檔案頭對程式頭的大小說明為非 0 值,且給出了它在檔案的偏移位址,在輸出資訊之中,包含了程式頭的詳細資訊。可看到,程式頭的 “Physical Addr” 描述了本程式要加載到的記憶體位址 “0x0800 0000”,正好是 STM3 2内部 FLASH 的首位址;“size in file” 描述了本程式占據的空間大小為 “1456 bytes”,它正是程式燒錄到 FLASH 中需要占據的空間。

3.4.1.4 節區頭

在 ELF 的原檔案中,緊接着程式頭的一般是節區的主體資訊,在節區主體資訊之後是描述節區主體資訊的節區頭.

====================================
 
 // Section #4

 Name : i.LED_GPIO_Config //節區名

 //此節區包含程式定義的資訊,其格式和含義都由程式來解釋。

 Type : SHT_PROGBITS (0x00000001)

 //此節區在程序執行過程中占用記憶體。節區包含可執行的機器指令。

 Flags :SHF_ALLOC + SHF_EXECINSTR (0x00000006)

 Addr : 0x00000000 //位址

 File Offset : 68 (0x44)        //在檔案中的偏移

 Size : 116 bytes (0x74) //大小

 Link : SHN_UNDEF

 Info : 0

 Alignment : 4 //位元組對齊

 Entry Size : 0

 ====================================
 
           

這個節區的名稱為 LED_GPIO_Config,它正好是我們在 bsp_led.c 檔案中定義的函數名,這個節區頭描述的是該函數被編譯後的節區資訊,其中包含了節區的類型(指令類型)、節區應存儲到的位址 (0x00000000)、它主體資訊在檔案位置中的偏移 (68) 以及節區的大小 (116 bytes)。

由于 .o 檔案是可重定位檔案,是以它的位址并沒有被配置設定,是 0x00000000(假如檔案中還有其它函數,該函數生成的節區中,對應的位址描述也都是 0)。當連結器連結時,根據這個節區頭資訊,在檔案中找到它的主體内容,并根據它的類型,把它加入到主程式中,并配置設定實際位址,連結後生成的 .axf 檔案,我們再來看看它的内容:

========================================================================

 ** Section #1

 Name : ER_IROM1 //節區名

 //此節區包含程式定義的資訊,其格式和含義都由程式來解釋。

 Type : SHT_PROGBITS (0x00000001)


 //此節區在程序執行過程中占用記憶體。節區包含可執行的機器指令

 Flags : SHF_ALLOC + SHF_EXECINSTR (0x00000006)

 Addr : 0x08000000 //位址

 File Offset : 52 (0x34)

 Size : 1456 bytes (0x5b0) //大小

 Link : SHN_UNDEF

 Info : 0

 Alignment : 4

 Entry Size : 0

 ====================================

 ** Section #2


 Name : RW_IRAM1 //節區名


 //包含将出現在程式的記憶體映像中的為初始

 //化資料。根據定義,當程式開始執行,系統

 //将把這些資料初始化為 0。

 Type : SHT_NOBITS (0x00000008)


 //此節區在程序執行過程中占用記憶體。節區包含程序執行過程中将可寫的資料。

 Flags : SHF_ALLOC + SHF_WRITE (0x00000003)

 Addr : 0x20000000 //位址

 File Offset : 1508 (0x5e4)

 Size : 1024 bytes (0x400) //大小

 Link : SHN_UNDEF

 Info : 0

 Alignment : 8

 Entry Size : 0

 ====================================
 
           

在 .axf 檔案中,主要包含了兩個節區,一個名為 ER_IROM1,一個名為 RW_IRAM1,這些節區頭資訊中除了具有 .o 檔案中節區頭描述的節區類型、檔案位置偏移、大小之外,更重要的是它們都有具體的位址描述,其中 ER_IROM1 的位址為 0x08000000,而 RW_IRAM1 的位址為 0x20000000,它們正好是内部 FLASH 及 SRAM 的首位址,對應節區的大小就是程式需要占用 FLASH 及 SRAM 空間的實際大小。

也就是說,經過連結器後,它生成的 .axf 檔案已經彙總了其它 .o 檔案的所有内容,生成的 ER_IROM1 節區内容可直接寫入到 STM32 内部 FLASH 的具體位置。例如,前面 .o 檔案中的 i.LED_GPIO_Config 節區已經被加入到 .axf 檔案的ER_IROM1 節區的某位址。

3.4.1.5 節區主體及反彙編代碼

使用 fromelf 的 -c 選項可以檢視部分節區的主體資訊,對于指令節區,可根據其内容檢視相應的反彙編代碼。

//.o檔案的LED_GPIO_Config節區及反彙編代碼

 ** Section #4 'i.LED_GPIO_Config' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]

 Size : 116 bytes (alignment 4)

 Address: 0x00000000

 $t

 i.LED_GPIO_Config

 LED_GPIO_Config

 // 位址内容 (ASCII碼) 内容對應的代碼

 // (無意義)

 0x00000000: e92d41fc -..A PUSH {r2-r8,lr}

 0x00000004: 2101 .! MOVS r1,#1

 0x00000006: 2088 . MOVS r0,#0x88

 0x00000008: f7fffffe .... BL RCC_AHB1PeriphClockCmd

 0x0000000c: f44f6580 O..e MOV r5,#0x400
 
 0x00000010: 9500 .. STR r5,[sp,#0]

 0x00000012: 2101 .! MOVS r1,#1

 0x00000014: f88d1004 .... STRB r1,[sp,#4]

 0x00000018: 2000 . MOVS r0,#0

 0x0000001a: f88d0006 .... STRB r0,[sp,#6]

 0x0000001e: f88d1007 .... STRB r1,[sp,#7]

 0x00000022: f88d0005 .... STRB r0,[sp,#5]

 0x00000026: 4f11 .O LDR r7,[pc,#68] ;

 0x00000028: 4669 iF MOV r1,sp

 0x0000002a: 4638 8F MOV r0,r7

 0x0000002c: f7fffffe .... BL GPIO_Init

 0x00000030: 006c l. LSLS r4,r5,#1

 /*....以下省略**/
 
           

可看到,由于這是 .o 檔案,它的節區位址還是沒有配置設定的,基位址為 0x00000000,接着在 LED_GPIO_Config 标号之後,列出了一個表,表中包含了位址偏移、相應位址中的内容以及根據内容反彙編得到的指令。細看彙編指令,還可看到它包含了跳轉到 RCC_AHB1PeriphClockCmd 及 GPIO_Init 标号的語句,而且這兩個跳轉語句原來的内容都是 “f7fffffe”,這是因為還 .o 檔案中并沒有 RCC_AHB1PeriphClockCmd 及 GPIO_Init 标号的具體位址索引,在 .axf 檔案中,這是不一樣的。

//.axf檔案的LED_GPIO_Config反彙編代碼
 i.LED_GPIO_Config

 LED_GPIO_Config

 0x080002a4: e92d41fc -..A PUSH {r2-r8,lr}

 0x080002a8: 2101 .! MOVS r1,#1

 0x080002aa: 2088 . MOVS r0,#0x88

 0x080002ac: f000f838 ..8. BL RCC_AHB1PeriphClockCmd ; 0x8000320

 0x080002b0: f44f6580 O..e MOV r5,#0x400

 0x080002b4: 9500 .. STR r5,[sp,#0]

 0x080002b6: 2101 .! MOVS r1,#1

 0x080002b8: f88d1004 .... STRB r1,[sp,#4]

 0x080002bc: 2000 . MOVS r0,#0

 0x080002be: f88d0006 .... STRB r0,[sp,#6]

 0x080002c2: f88d1007 .... STRB r1,[sp,#7]

 0x080002c6: f88d0005 .... STRB r0,[sp,#5]

 0x080002ca: 4f11 .O LDR r7,[pc,#68] ; [0x8000310] = 0x40021c00

 0x080002cc: 4669 iF MOV r1,sp

 0x080002ce: 4638 8F MOV r0,r7

 0x080002d0: f7ffffa5 .... BL GPIO_Init ; 0x800021e

 0x080002d4: 006c l. LSLS r4,r5,#1

 /*....以下省略**/
 
           

可看到,除了基位址以及跳轉位址不同之外,LED_GPIO_Config 中的内容跟 .o 檔案中的一樣。另外,由于 .o 是獨立的檔案,而 .axf 是整個工程彙總的檔案,是以在 .axf 中包含了所有調用到 .o 檔案節區的内容。

在 .axf 檔案中,跳轉到 RCC_AHB1PeriphClockCmd 及 GPIO_Init 标号的這兩個指令後都有注釋,分别是 “; 0x8000320” 及 “; 0x800021e”,它們是這兩個标号所在的具體位址,而且這兩個跳轉語句的跟 .o 中的也有差別,内容分别為 “f000f838e” 及 “f7ffffa5”( .o中的均為f7fffffe)。這就是連結器連結的含義,它把不同 .o 中的内容連結起來了。

3.4.1.6 分散加載代碼

學習至此,還有一個疑問,前面提到程式有存儲态及運作态,它們之間應有一個轉化過程,把存儲在 FLASH 中的 RW-data 資料拷貝至 SRAM。然而我們的工程中并沒有編寫這樣的代碼,在彙編檔案中也查不到該過程,晶片是如何知道 FLASH 的哪些資料應拷貝到 SRAM 的哪些區域呢?程式中具有一段名為 “__scatterload” 的分散加載代碼,它是由 armlink 連結器自動生成的.

//分散加載代碼

 .text

 __scatterload

 __scatterload_rt2

 0x080001e4: 4c06 .L LDR r4,[pc,#24] ; [0x8000200] = 0x80005a0

 0x080001e6: 4d07 .M LDR r5,[pc,#28] ; [0x8000204] = 0x80005b0

 0x080001e8: e006 .. B 0x80001f8 ; __scatterload + 20

 0x080001ea: 68e0 .h LDR r0,[r4,#0xc]

 0x080001ec: f0400301 @... ORR r3,r0,#1

 0x080001f0: e8940007 .... LDM r4,{r0-r2}

 0x080001f4: 4798 .G BLX r3

 0x080001f6: 3410 .4 ADDS r4,r4,#0x10

 0x080001f8: 42ac .B CMP r4,r5

 0x080001fa: d3f6 .. BCC 0x80001ea ; __scatterload + 6

 0x080001fc: f7ffffda .... BL __main_after_scatterload ; 0x80001b4

 $d

 0x08000200: 080005a0 .... DCD 134219168

 0x08000204: 080005b0 .... DCD 134219184
 
           

這段分散加載代碼包含了拷貝過程( LDM 複制指令),而 LDM 指令的操作數中包含了加載的源位址,這些位址中包含了内部 FLASH 存儲的 RW-data 資料。而 "__scatterload " 的代碼會被 “__main” 函數調用,__main 在啟動檔案中的 “Reset_Handler” 會被調用,因而,在主體程式執行前,已經完成了分散加載過程。

_main
 
 _main_stk

 0x080001ac: f8dfd00c .... LDR sp,__lit__00000000 ; [0x80001bc] = 0x20000400

 .ARM.Collect$$$$00000004

 _main_scatterload

 0x080001b0: f000f818 .... BL __scatterload ; 0x80001e4
 
           

3.5 hex檔案及bin檔案

若編譯過程無誤,即可把工程生成前面對應的 .axf 檔案,而在 MDK 中使用下載下傳器 (DAP/JLINK/ULINK 等)下載下傳程式或仿真的時候,MDK 調用的就是 .axf 檔案,它解釋該檔案,然後控制下載下傳器把 .axf 中的代碼内容下載下傳到 STM32 晶片對應的存儲空間,然後複位後晶片就開始執行代碼了。

然而,脫離了 MDK 或 IAR 等工具,下載下傳器就無法直接使用 .axf 檔案下載下傳代碼了,它們一般僅支援 hex 和 bin 格式的代碼資料檔案。預設情況下 MDK 都不會生成 hex 及 bin 檔案,需要配置工程選項或使用 fromelf 指令。

3.5.1 生成hex檔案

生成 hex 檔案的配置比較簡單,在 “Options for Target->Output->Create Hex File” 中勾選該選項,然後編譯工程即可,見下圖 :

MDK的編譯過程及檔案類型全解——(二)

3.5.2 生成bin檔案

使用 MDK 生成 bin 檔案需要使用 fromelf 指令,在 MDK 的 “Options For Target->Users” 中加入下圖的指令。

MDK的編譯過程及檔案類型全解——(二)

圖中的指令内容為:

“fromelf --bin --output …\Output\多彩流水燈.bin …\Output\多彩流水燈.axf”

該指令是根據本機及工程的配置而寫的,在不同的系統環境或不同的工程中,指令内容都不一樣,我們需要了解它,才能為自己的工程定制指令,首先看看 fromelf 的幫助,見下圖:

MDK的編譯過程及檔案類型全解——(二)

我們在 MDK 輸入的指令格式是遵守 fromelf 幫助裡的指令格式說明的,其格式為:

“fromelf [options] input_file”

其中 optinos 是指令選項,一個指令支援輸入多個選項,每個選項之間使用空格隔開,我們的執行個體中使用 “–bin” 選項設定輸出 bin 檔案,使用 “–output file” 選項設定輸出檔案的名字為 “…\Output\多彩流水燈.bin”,這個名字是一個相對路徑格式,如果不了解如何使用 “…” 表示路徑,可使用 MDK 指令輸入框後面的檔案夾圖示打開檔案浏覽器選擇檔案,在指令的最後使用 “…\Output\多彩流水燈.axf” 作為指令的輸入檔案。具體的格式分解見下圖:

MDK的編譯過程及檔案類型全解——(二)

fromelf 需要根據工程的 .axf 檔案輸入來轉換得到 bin 檔案,是以在指令的輸入檔案參數中要選擇本工程對應的 .axf 檔案,在 MDK 指令輸入欄中,我們把 fromelf 指令放置在 “After Build/Rebuild” (工程建構完成後執行)一欄也是基于這個考慮,這樣設定後,工程建構完成生成了最新的 .axf 檔案,MDK 再執行 fromelf 指令,進而得到最新的 bin 檔案。

設定完成生成 hex 的選項或添加了生成 bin 的使用者指令後,點選工程的編譯 (build)按鈕,重新編譯工程,成功後可看到下圖中的輸出。打開相應的目錄即可找到檔案,若找不到 bin 檔案,請檢視提示輸出欄執行指令的資訊,根據資訊改正 fromelf 指令。

MDK的編譯過程及檔案類型全解——(二)

其中 bin 檔案是純二進制資料,無特殊格式,接下來我們了解一下 hex 檔案格式。

3.5.3 hex檔案格式

hex 是 Intel 公司制定的一種使用 ASCII 文本記錄機器碼或常量資料的檔案格式,這種檔案常常用來記錄将要存儲到 ROM 中的資料,絕大多數下載下傳器支援該格式。

一個 hex 檔案由多條記錄組成,而每條記錄由五個部分組成,格式形如 “: ll aaaatt[dd…]cc”,例如本"多彩流水燈"工程生成的 hex 檔案前幾條記錄如下:

:020000040800F2

:1000000000040020C10100081B030008A30200082F

:100010001903000809020008690400080000000034

:100020000000000000000000000000003D03000888

:100030000B020008000000001D0300081504000862

:10004000DB010008DB010008DB010008DB01000820

           

記錄的各個部分介紹如下:

(1) “:” :每條記錄的開頭都使用冒号來表示一條記錄的開始;

(2) ll :以 16 進制數表示這條記錄的主體資料區的長度(即後面[dd…]的長度);

(3) aaaa:表示這條記錄中的内容應存放到 FLASH 中的起始位址;

(4) tt:表示這條記錄的類型,它包含中的各種類型;

MDK的編譯過程及檔案類型全解——(二)

(5) dd:表示一個位元組的資料,一條記錄中可以有多個位元組資料,ll 區表示了它有多少個位元組的資料;

(6) cc:表示本條記錄的校驗和,它是前面所有 16 進制資料 (除冒号外,兩個為一組)的和對 256 取模運算的結果的補碼。

例如,上面的第一條記錄解釋如下:

(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:表示這條記錄資料區的長度為 16 位元組;

(2) 0000:表示這條記錄所在的位址,與前面的擴充記錄結合,表示這條記錄要存儲的 FLASH 首位址為(0x0800 0000+0x0000);

(3) 00:表示這是一條資料記錄,資料區的是位址;

(4) 00040020C10100081B030008A3020008:這是要按位址存儲的資料;

(5) 2F:校驗和

為了更清楚地對比 bin、hex 及 axf 檔案的差異,我們來檢視這些檔案内部記錄的資訊來進行對比。

3.5.4 hex、bin及axf檔案的差別與聯系

bin、hex 及 axf 檔案都包含了指令代碼,但它們的資訊豐富程度是不一樣的。

(1) bin 檔案是最直接的代碼映像,它記錄的内容就是要存儲到 FLASH 的二進制資料(機器碼本質上就是二進制資料),在 FLASH 中是什麼形式它就是什麼形式,沒有任何輔助資訊,包括大小端格式也沒有,是以下載下傳器需要有針對晶片 FLASH 平台的輔助檔案才能正常下載下傳(一般下載下傳器程式會有比對的這些資訊);

(2) hex 檔案是一種使用十六進制符号表示的代碼記錄,記錄了代碼應該存儲到 FLASH 的哪個位址,下載下傳器可以根據這些資訊輔助下載下傳;

(3) axf 檔案在前文已經解釋,它不僅包含代碼資料,還包含了工程的各種資訊,是以它也是三個檔案中最大的。

同一個工程生成的 bin、hex 及 axf 檔案的大小見下圖:

MDK的編譯過程及檔案類型全解——(二)

實際上,這個工程要燒寫到 FLASH 的内容總大小為 1456 位元組,然而在 Windows 中檢視的 bin 檔案卻比它大( bin 檔案是 FLASH 的代碼映像,大小應一緻),這是因為 Windows 檔案顯示機關的原因,使用右鍵檢視檔案的屬性,可以檢視它實際記錄内容的大小,見下圖:

MDK的編譯過程及檔案類型全解——(二)

接下來我們打開本工程的 “多彩流水燈.bin”、“多彩流水燈 .hex “及由"多彩流水燈.axf” 使用 fromelf 工具輸出的反彙編檔案"多彩流水燈 _axf_elfInfo_c.txt” 檔案,清晰地對比它們的差異,見下圖。如果您想要親自閱讀自己電腦上的 bin 檔案,推薦使用 sublime 軟體打開,它可以把二進制數以 ASCII 碼呈現出來,便于閱讀。

MDK的編譯過程及檔案類型全解——(二)

在 hex 檔案中包含了位址資訊以及位址中的内容,而在 bin 檔案中僅包含了内容,連存儲的位址資訊都沒有。觀察可知,bin、hex及axf 檔案中的資料内容都是相同的,它們存儲的都是機器碼。這就是它們三都之間的差別與聯系。

由于檔案中存儲的都是機器碼,見下圖,該圖是根據 axf 檔案的 GPIO_Init 函數的機器碼,在 bin 及 hex 中找到的對應位置。是以經驗豐富的人是有可能從 bin 或 hex 檔案中恢複出彙編代碼的,隻是成本較高,但不是不可能。

MDK的編譯過程及檔案類型全解——(二)

如果晶片沒有做任何加密措施,使用下載下傳器可以直接從晶片讀回它存儲在 FLASH 中的資料,進而得到 bin 映像檔案,根據晶片型号還原出部分代碼即可進行修改,甚至不用修改代碼,直接根據目标産品的硬體 PCB,抄出一樣的闆子,再把 bin 映像下載下傳晶片,直接山寨出目标産品,是以在實際的生産中,一定要注意做好加密措施。由于 axf 檔案中含有大量的資訊,且直接使用 fromelf 即可反彙編代碼,是以更不要随便洩露 axf 檔案。lib 檔案也能反使用 fromelf 檔案反彙編代碼,不過它不能還原出 C 代碼,由于 lib 檔案的主要目的是為了保護 C 源代碼,也算是達到了它的要求。

3.5.5 htm靜态調用圖檔案

Output 目錄下,有一個以工程檔案命名的字尾為 .bulid_log.htm 及 .htm 檔案,如"多彩流水燈 .bulid_log.htm" 及"多彩流水燈 .htm",它們都可以使用浏覽器打開。其中 .build_log.htm 是工程的建構過程日志,而 .htm 是連結器生成的靜态調用圖檔案。

在靜态調用圖檔案中包含了整個工程各種函數之間互相調用的關系圖,而且它還給出了靜态占用最深的棧空間數量以及它對應的調用關系鍊。

MDK的編譯過程及檔案類型全解——(二)

該檔案說明了本工程的靜态棧空間最大占用 56 位元組 (Maximum Stack Usage:56bytes),這個占用最深的靜态調用為 “main->LED_GPIO_Config->GPIO_Init”。注意這裡給出的空間隻是靜态的棧使用統計,連結器無法統計動态使用情況,例如連結器無法知道遞歸函數的遞歸深度。在本檔案的後面還可查詢到其它函數的調用情況及其它細節。

利用這些資訊,我們可以大緻了解工程中應該配置設定多少空間給棧,有空間餘量的情況下,一般會設定比這個靜态最深棧使用量大一倍,在 STM32 中可修改啟動檔案改變堆棧的大小;如果空間不足,可從該檔案中了解到調用深度的資訊,然後優化該代碼。

注意:

檢視了各個工程的靜态調用圖檔案統計後,我們發現本書提供的一些比較大規模的工程例子,靜态棧調用最大深度都已超出 STM32 啟動檔案預設的棧空間大小 0x00000400,即 1024 位元組,但在當時的調試過程中卻沒有發現錯誤,是以我們也沒有修改棧的預設大小(有一些工程調試時已發現問題,它們的棧空間就已經被我們改大了),雖然這些工程實際運作并沒有錯誤,但這可能隻是因為它使用的棧溢出 RAM 空間恰好沒被程式其它部分修改而已。是以,建議您在實際的大型工程應用中(特别是使用了各種外部庫時,如 Lwip/emWin/Fatfs 等),要檢視本靜态調用圖檔案,了解程式的棧使用情況,給程式配置設定合适的棧空間。(關于 Listing 目錄下的介紹。請看下文——MDK的編譯過程及檔案類型全解——(三))

轉載自—第48章 MDK的編譯過程及檔案類型全解

繼續閱讀