“ARM 程序”是指在 ARM 系統中正在執行的程序,而非保存在 ROM 中的 bin 映像(image)文件。這一點清注意區别。
一個 ARM 程序包含 3 部分: RO, RW 和 ZI。
由以上 3 點說明可以理解為:
1.1 ARM 芯片的啟動過程概述
圖 3.1 ARM 芯片的啟動過程詳解
注意,以上的過程并非絕對的,不同的 ARM 架構或是不同的代碼以上的執行過程是不同的。
複位處理程序是在彙編器中編寫的短模塊,系統一啟動就立即執行。複位處理程序最少要為應用程序的運行模式初始化堆棧指針。對于具有本地内存系統(如緩存、 TCM、 MMU 和MPU)的處理器,某些配置必須在初始化過程的這一階段完成。複位處理程序在執行之後,通常跳轉到__main 以開始 C 庫初始化序列。
__main 負責設置内存,而__rt_entry 負責設置運行時環境。 __main 執行代碼和數據複制、解壓縮以及 ZI 數據的零初始化。然後,它跳轉到__rt_entry,設置堆棧和堆、初始化庫函數和靜态數據,并調用任何頂級 C 構造函數。然後, __rt_entry 跳轉到應用程序的入口 main()。主應用程序結束執行後, __rt_entry 将庫關閉,然後把控制權交還給調試器。函數标簽 main()具有特殊含義。 main()函數的存在強制鍊接器鍊接到__main 和__rt_entry 中的初始化代碼。如果沒有标記為 main()的函數,則沒有鍊接到初始化序列,因而部分标準 C 庫功能得不到支持。
1.2 結合代碼來看 ARM 芯片的啟動過程
1.2.1 調試環境的搭建及測試代碼
使用 Keil for ARM( uVision4)的軟件模拟,工程的硬件設定為 LPC1700 系列,不使用microlib。這裡不使用 microlib,則系統自動加載标準 C Library,這樣我們才能看到标準的 ARM芯片的标準啟動過程。随後我們會對 microlib 進行探讨。測試代碼如下:
程序清單 3.1 啟動過程測試代碼#include "LPC17xx.h" /* LPC17xx 外設寄存器 */int main (void){
SystemInit();//系統初始化
while (1) {}
}
我們的測試代碼使用的是一段最簡單的代碼,代碼本身隻包含 CMSIS 标準的必備文件,即stdint.h、 core_cm3.h、 core_cm3.c、 system_LPC17xx.h、 system_LPC17xx.c、 LPC17xx.h、startup_LPC17xx.s 和 main.c。
1.2.2 跟蹤啟動代碼開始調試之前,須将工程設置的 DEBUG 欄中取消掉 Run to main()的勾選,否則代碼會直接運行到 main()函數,我們也就無法看到芯片的啟動過程了。然後啟動調試,最先進入 Reset_Handler,如下圖所示。
圖 1.2 Reset_Handler
繼續單步運行,程序跳入__main(C Library 的代碼,并非用戶代碼),如下圖所示。
圖 1.3 __main
繼續單步運行,經過一系列的代碼(主要是 scatter load 過程)後,程序進入__rt_entry(同樣是由 C Library 管理,并非用戶代碼)。
圖 1.4 __rt_entry
繼續單步運行,再次經過一系列的代碼之後(主要是堆棧和堆的初始化以及 C Library 的初始化),程序進入 main()。如下圖所示。
圖 1.5 進入 main 函數
以上是整個測試代碼啟動過程的跟蹤調試的大緻過程,這個過程對 ARM 系列的芯片來說都是相同的,不同的是裡面具體的細節。
1.2.3 詳細的啟動過程使用的依然是上面的測試代碼,詳細啟動過程如下圖所示。
圖 1.6 芯片啟動的詳細過程
上圖顯示的就是測試代碼在 LPC17xx 上啟動運行的全過程(詳細過程),由此圖可見LPC17xx 的啟動過程與圖 1.1 所示的啟動過程是基本一緻的,但是還有差别,可以說是圖 1.1所示啟動過程的簡化版。注意:并非所有代碼的啟動過程全部相同,此啟動過程與所使用的鍊接器、用戶代碼以及其所集成的 C Library 密切相關。此圖為由“armlink”的鍊接器為程序清單 1.2 所示的測試代碼産生的啟動過程,其他情況可能會有一些差異(比如有的代碼的啟動過程就會運行一段 RW段的解壓代碼等,而本例程中則沒有)。
1.2.4 __main
若程序使用的是 C 或 C 語言編寫的代碼,那麼 C/C 程序的入口是在 C Library 中的__main。庫代碼在此處執行以下操作。
1.2.5 __rt_entry
__rt_entry 符号是使用 ARM C 庫的程序的起點。将所有分散加載區重定位到其執行地址後,會将控制權傳遞給__rt_entry。其有如下缺省實現:
注意:最後兩步是在程序退出 main()函數時才會執行,而嵌入式程序一般都是死循環,所以基本不會執行這兩個過程。還有,以上過程是對标準 C Library 而言,不包括使用 microlib的情況。
1.2.6 __rt_lib_init
這是庫初始化函數,它與__rt_lib_shutdown()配合使用。
這是庫初始化函數。它是緊靠__rt_stackheap_init()後面調用的,并傳遞一個要用作堆的初始内存塊。此函數是标準 ARM 庫初始化函數,不能重新實現此函數。
1.3 關于 microlib
microlib 是缺省 C 庫的備選庫。它旨在與需要裝入到極少量内存中的深層嵌入式應用程序配合使用。這些應用程序不在操作系統中運行。 microlib 進行了高度優化以使代碼變得很小。它的功能比缺省 C 庫少,并且根本不具備某些 ISO C 特性。某些庫函數的運行速度也比較慢,例如, memcpy()。microlib 與缺省 C 庫之間的主要差異是:
1.4 x.map
想要更好的了解啟動代碼的運行機制,我們就有必要了解一下由 Keil 的鍊接器“armlink”生成的描述文件即 x.map 文件。
1.4.1 關于鍊接器
圖 1.7 目标文件的組成
上圖即是 armlink 的鍊接器為程序清單 1.1 所示的測試代碼生成的.map 文件中的一部分,其描述了鏡像文件的組成信息,其中可以明顯的看到其由兩部分構成:
可見我們在上文中所描述的啟動過程中看到的__main、 __rt_entry、 __scatterload 以及__rt_lib_init 等,就是 C Library 中的代碼。同理,我們每次燒錄的可執行的 ARM 的 bin 文件中不僅有開發者編寫的代碼,還有C Library 的代碼。
1.4.2 RW 段在 RAM 的存放
圖 1.8 由 armlink 生成的 RW 段在 RAM 中存放的描述
1.5 關于 ARM 程序的 Memory 管理
1.5.1 ARM 鏡像文件的組成(image)
所謂 ARM 映像文件就是指燒錄到 ROM 中的 bin 文件,也成為 image 文件。以下用 Image文件來稱呼它。 Image 文件包含了 RO 和 RW 數據(注意:不包含 ZI 數據)。之所以 Image 文件不包含 ZI 數據,是因為 ZI 數據都是 0,沒必要包含,隻要程序運行之前将 ZI 數據所在的區域(執行區域)一律清零即可。包含進去反而浪費存儲空間。
1.5.2 關于 image 文件(鏡像文件)
從以上兩點可以知道,燒錄到 ROM 中的 image 文件與實際運行時的 ARM 程序之間并不是完全一樣的。因此就有必要了解 ARM 程序是如何從 ROM 中的 image 到達實際運行狀态的。實際上, RO 中的指令(啟動程序)至少應該有這樣的功能:
1.5.3 RO 段
看下面兩段程序,它們之間差了一條語句,這條語句就是聲明一個字符常量。因此按照之前的内容,它們之間應該隻會在 RO 數據中相差一個字節(字符常量為 1 字節)。
Prog1:
#include <stdio.h>
void main(void)
{
;
}
Prog2:
#include <stdio.h>
const char a = 5;
void main(void){
;
}
Prog1 編譯出來後的信息如下(來自.map 文件):
Prog2 編譯出來後的信息如下(來自.map 文件):
以上兩個程序編譯出來後的信息可以看出: Prog1 和 Prog2 的 RO 包含了 Code 和 RO Data兩類數據。他們的唯一區别就是 Prog2 的 RO Data 比 Prog1 多了 1 個字節。這正和之前的推測一緻。如果增加的是一條指令而不是一個常量,則結果應該是 Code 數據大小有差别。
1.5.4 RW 段
同樣再看兩個程序,它們之間隻相差一個“已初始化的變量”,按照之前所講的,已初始化的變量應該是算在 RW 中的,所以兩個程序之間應該是 RW 大小有區别。Prog3:
#include <stdio.h>
void main(void)
{;
}
Prog4:
#include <stdio.h>
char a = 5;
void main(void)
{
;
}
Prog3 編譯出來後的信息如下(來自.map 文件):
Prog4 編譯出來後的信息如下(來自.map 文件):
以上兩個程序編譯出來後的信息可以看出: Prog1 和 Prog2 的 RO 包含了 Code 和 RO Data兩類數據。他們的唯一區别就是 Prog2 的 RO Data 比 Prog1 多了 1 個字節。這正和之前的推測一緻。如果增加的是一條指令而不是一個常量,則結果應該是 Code 數據大小有差别。
1.5.5 ZI 段(初始化為 0 或未初始化的變量)
再看兩個程序,他們之間的差别是一個未初始化的變量“a”,從之前的了解中,應該可以推測,這兩個程序之間應該隻有 ZI 大小有差别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a;
void main(void)
{
;
}
Prog3 編譯出來後的信息如下(來自.map 文件):
Prog4 編譯出來後的信息如下(來自.map 文件):
編譯的結果完全符合推測,隻有 ZI 數據相差了 1 個字節。這個字節正是未初始化的一個字符型變量“a”所引起的。
注意: 如果一個變量被初始化為 0,則該變量的處理方法與未初始化華變量一樣放在 ZI區域。即: ARM C 程序中,所有的未初始化變量都會被自動初始化為 0。以上代碼是再 ADS 下編譯的, keil 環境下與之不同,比如在 keil 下生成 ZI 數據段就必須定義一個大于 8 字節的未初始化或初始化位 0 的變量,且必須在源代碼中引用此變量才會在鍊接的描述文件中看到其生成的 ZI 文件。
1.6 缺省内存映射
對于沒有描述内存映射的映像,鍊接器根據缺省内存映射放置代碼和數據。如下圖所示。
圖 1.9 缺省内存映射
注意:這個内存映射并非對所有芯片都有效,不同的芯片的内存映射是不同的。在
Cortex-M3 中的内存映射與上圖是一緻的。
1.7 内存模型
在 Keil for ARM 下你可以選擇以下任意内存模型:
1. 單内存區
堆棧從内存區頂部向下增長。堆從内存區底部向上增長。這是缺省設置。由堆管理的内存從來不會縮減。不能将通過調用 free()釋放的堆内存再次用于其他用途。
2. 雙内存區
一個内存區用于堆棧,另一個内存區用于堆。堆區大小可以是零。堆棧區可以位于分配的内存中,也可以從執行環境中繼承。要使用雙區模型而不是缺省的單區模型,請使用以下任一方法:
例如下圖所示,此代碼來自 startup_LPC17xx.s。
如果使用雙區内存模型,并且未提供任何堆内存,則無法調用 malloc()、使用 stdio 或獲取main()的命令行參數。如果将堆區大小設置為 0,并且将__user_heap_extend()定義為可擴展堆的函數,則會在需要時創建堆。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!