從燒錄程序到設備加載運行

前言

    在最近的一個項目中,突然想起以前學習嵌入式系統啓動流程,所以藉此機會複習一下從燒錄程序到設備加載代碼運行的過程,加深印象。

一.程序的結構

一個程序一般分爲3段:text段,data段,bss段

text段:就是放程序代碼的,編譯時確定,只讀,

data段:存放在編譯階段(而非運行時)就能確定的數據,可讀可寫

就是通常所說的靜態存儲區,賦了初值的全局變量和靜態變量存放在這個區域,常量也存放在這個區域

bss段:定義而沒有賦初值的全局變量和靜態變量,放在這個區域

我們編譯完成後生成燒錄文件,一般單片機的HEX文件,還有些其他類型的文件,內容基本上都是上述三個段。接下來用燒錄工具把生成的文件燒錄到機器中的ROM或者FLASH。

二.設備啓動

    以一般的單片機或者嵌入式Linux產品爲例,CPU芯片中一般還有兩個片內的ROM和RAM,ROM中存着一段芯片廠家出廠就寫好的指令。機器啓動時,CPU會運行ROM中的指令,把Flash中bootloader的第一部分,加載到CPU的RAM中,這個階段做的事是:

1.硬件設備初始化:CPU的工作模式、關看門狗、設置時鐘、關MMU、關CACHE等 (代碼在cpu/arm920t/start.S中reset:)
2.爲加載Bootloader的第二階段代碼準備RAM空間:初始化內存芯片SDRAM,使得外接 的SDRAM可用 (代碼在board/smdk2410/lowlevel_Init.S中的lowlevel.Init中,是在start.S中調用的)
3.複製Bootloader的第二階段代碼在RAM空間中:這裏將整個U-boot的代碼(包括第一、 第二階段)都複製到SDRAM中 (代碼在cpu/arm920t/start.S中實現)
4.設置好堆棧:堆棧的靈活性很大,只要讓sp寄存器指向一段沒有使用的內存即可 (代碼在cpu/arm920t/start.S中實現)
5.清除BSS段之後,跳轉到第二階段的C代碼入口點:ldrpc,_start_armboot _start_armboot:.wordstart_armboot (代碼在cpu/arm920t/start.S中實現,被調用的函數start_armboot在lib_arm/board.c中)

第二階段:

6.初始化本階段要使用的硬件設備:如設置系統時鐘、初始化串口。注意board_init函數 還保存了機器類型的ID (如代碼在board/smdk2410/smdk2410.c中的board_init、在cpu/arm920t/s3c24x0/srial.c中的serial.init)

7.檢測系統內存映射:確定板上使用了多少內存、它們的地址空間是什麼,檢測到的參數 在向內核傳遞參數的時候用到 (代碼在board/smdk2410/smdk2410.c中的dram_init)
8.將內核映像和根文件系統映像從Flash上讀到RAM空間中。內核的複製和啓動,這裏 是通過命令bootm、bootp、nboot來完成的,這些命令實際上是調用相應的函數,先將映像從各種媒介中讀出,存放在指定的位置 (u-boot中的命令都是通過U_BOOT_CMD宏來定義的, U_BOOT_CMD(name,maxargs,repeatable,command,"usage","help"))
9. 爲內核{設置}啓動參數:U-Boot也是通過【標記列表】向內核傳遞參數。一般而言只設置內存標記-取值函數setup_memory_tags和命令行標記-取值setup_commandline_tag就可以了。這一步很簡單,僅僅是配置對應的兩個宏就可以了(這兩個函數在lib_arm/armlinux.c中定義)
10.調用內核。對於ARM架構的CPU,都是通過lib_arm/armlinux.c中的do_bootm_linux函 數來啓動內核。在這個函數中,先設置標記列表,最後通過theKernel(0,bd->bi_arch_number,bd->bi_boot_params);調用內核。 theKernel指向內核存放的地址(對於ARM架構的CPU,通常是0x30008000),bd->bi_arch_number就是前面board_init函數設置的機器類型ID,而bd_bi_boot_params就是標記列表的開始地址(lib_arm/armlinux.c)

注意:
1、上面的有些步奏可能有些不是必需的、可以調換順序,比如在S3C2410/S3C2440的開發板中使用的U-Boot中,就是將CPU的速度和時鐘頻率的設置放到第二階段;
2、Flash上的內核映像有可能是經過壓縮的,在讀到RAM之後,還要進行解壓,當然,對於有自解壓的功能的內核,不需要Bootloadr來解壓
3、將根文件系統映像複製到RAM中也不是必須的,這取決於是什麼類型的根文件系統,以及內核訪問它的方法
4、甚至,將第二階段的代碼複製到RAM空間也不是必需的,對於NORFlash等存儲設備,完全可以在上面直接執行代碼,只不過相比在RAM中執行效率大爲降低


對於一般的單片機,可能沒有那麼複雜,簡單的來說,這兩個階段就是,初始化外部的RAM,初始化其他的硬件設備,把FLash中的text, data,bss三段加載到RAM中,加載的過程如下:

(1)爲全局變量分配地址空間---如果全局變量已賦初值,則將初始值從ROM中拷貝到RAM中,如果沒有賦初值,則這個全局變量所對應的地址下的初值爲0或者是不確定的。當然,如果已經指定了變量的地址空間,則直接定位到對應的地址就行,那麼這裏分配地址及定位地址的任務由“連接器”完成。

(2)設置堆棧段的長度及地址---用C語言開發的單片機程序裏面,普遍都沒有涉及到堆棧段長度的設置,但這不意味着不用設置。堆棧段主要是用來在中斷處理時起“保存現場”及“現場還原”的作用,其重要性不言而喻。而這麼重要的內容,也包含在了編譯器預設的內容裏面,確實省事,可並不一定省心。
(3)分配數據段data,常量段const,代碼段code的起始地址——代碼段與常量段的地址可以不管,它們都是固定在ROM裏面的,無論它們怎麼排列,都不會對程序產生影響。但是數據段的地址就必須得關心。數據段的數據時要從ROM拷貝到RAM中去的,而在RAM中,既有數據段data,也有堆棧段stack,還有通用的工作寄存器組。通常,工作寄存器組的地址是固定的,這就要求在絕對定址數據段時,不能使數據段覆蓋所有的工作寄存器組的地址。必須引起嚴重關注。
注:這裏所說的“第一行代碼處”,並不一定是你自己寫的程序代碼,絕大部分都是編譯器代勞的,或者是編譯器自帶的demo程序文件。因爲,你自己寫的程序(C語言程序)裏面,並不包含這些內容。高級一點的單片機,這些內容,都是在startup的文件裏面。
4、普通的flashMCU是在上電時或復位時,PC指針裏面的存放的是“0000”,表示CPU從ROM的0000地址開始執行指令,在該地址處放一條跳轉指令,使程序跳轉到_main函數中,然後根據不同的指令,一條一條的執行,當中斷髮生時(中斷數量也很有限,2~5箇中斷),按照系統分配的中斷向量表地址,在中斷向量裏面,放置一條跳轉到中斷服務程序的指令,如此如此,整個程序就跑起來了。決定CPU這樣做,是這種ROM結構所造成的。
注:特別的,如下

1--I/O口寄存器:也是可以被改變的量,它被安排在一個特別的RAM地址,爲系統所訪問,而不能將其他變量定義在這些位置。

2--中斷向量表:中斷向量表是被固定在MCU內部的ROM地址中,不同的地址對應不同的中斷。每次中斷產生時,直接調用對應的中斷服務子程序,將程序的入口地址放在中斷向量表中。

總結有如下幾段:

1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。

2、堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表,呵呵。

3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束後有系統釋放

4、文字常量區—常量字符串就是放在這裏的。 程序結束後由系統釋放

5、程序代碼區—存放函數體的二進制代碼。

如果哪位有緣人看到覺得有幫助,就隨手端個讚唄~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章