13.4 C語言程序的運行
在嵌入式系統中,程序最終是要放置在內存中運行的,程序的幾個段,最終會轉化爲內存中的幾個區域。C語言可執行程序的內存佈局如圖13-5所示。
圖13-5 C語言可執行程序的內存佈局
在內存中,從低地址到高地址,依次是隻讀段、讀寫段、未初始化數據段、堆段、棧段。
映像文件中將包含代碼段(Code)、只讀數據段(RO Data)以及讀寫數據段(RW Data),未初始化數據段(BSS)將在程序的初始化階段中開闢,堆棧在程序運行時動態開闢。
只讀區(RO)包括了代碼和只讀數據,在內存區域中,代碼段(Code)和只讀數據段(Ro Data)的存放形式上基本沒有區別。
對於程序運行時的內存使用,堆和棧一般是相向擴展的。堆的分配由程序決定,棧由編譯器管理。
在以上概念中,只是一種內存分佈,並沒有考慮實際系統的情況。在實際的系統中,程序有載入和運行兩個概念。嵌入式系統由兩種內存,一種是可以固化只讀的內存(如:ROM,Nor Flash),另一種是易失的可讀寫的內存(如:SRAM和SDRAM)。程序中的各個段也有需要固化和需要讀寫的。程序中的各段必須載入到內存的恰當位置,程序纔可以運行。C語言各部分的需要固化和可寫的情況如表13-2所示。
表13-2 C語言各部分的需要支持固化和可寫的情況
段 |
需要固化 |
需要可寫 |
代碼(Code) |
是 |
否 |
只讀數據(RO data) |
是 |
否 |
讀寫數據(RW data) |
是 |
是 |
未初始化數據(BSS) |
否 |
是 |
堆(heap) |
否 |
是 |
棧(stack) |
否 |
是 |
在嵌入式系統中,經過編譯的C語言程序可以通過操作系統運行,也可以在沒有操作系統的情況下運行。程序存放的位置和運行的位置通常是不一樣的。
一般情況下,經過編譯後的程序存儲在Flash或者硬盤中,在運行時需要將程序加載到RAM中。嵌入式系統的Nor Flash和硬盤還有一定的差別,在硬盤的程序必須加載到RAM中才可以運行,但是在Nor Flash中的程序可以通過XIP(eXcutive In Place)的方式運行。
在嵌入式系統中,C語言程序的運行包括3種類型:第一種是調試階段的程序運行,這個階段程序存放的位置和運行的位置是相同的;第二種是程序直接在Flash中運行(XIP);第三種是將Flash或者硬盤中的程序完全加載到RAM中運行。
在C語言程序的運行中,存在着兩個基本的內存空間,一個是程序的存儲空間,另一個是程序的運行空間。程序的存儲空間必須包括代碼段、只讀數據段和讀寫數據段,程序的加載區域必須包括讀寫數據段和未初始化數據段如表13-3所示。
表13-3 C語言各部分使用的存儲空間
段 |
代碼 |
只讀數據 |
讀寫數據 |
未初始化數據 |
程序的存儲空間(ROM) |
需要 |
不需要 |
||
程序的加載空間(RAM) |
不需要 |
需要 |
由於程序放入系統後,必須包括所有需要的信息,代碼表示要運行的機器代碼,只讀數據和讀寫數據包含程序中預先設置好的數據值,這些都是需要固化存儲的,但是未初始化數據沒有初值,因此只需要標示它的大小,而不需要存儲區域。
在程序運行的初始化階段,將進行加載動作,其中讀寫數據和未初始化數據都是要在程序中進行“寫”操作,因此不可能放在只讀的區域內,必須放入RAM中。當然,程序也可以將代碼和只讀數據放入RAM。
在程序運行後,堆和棧將在程序運行過程中動態地分配和釋放。
13.4.1 RAM調試運行
本節介紹程序的一種特殊的運行方式,即在程序的調試階段將主機的映像文件直接放置到目標系統的RAM中。在這種應用中,RAM既是程序的存儲空間,也是程序的運行空間。
在嵌入式系統中,這是一種常用的調試方式,而不是通常的運行方式。在通常的運行方式下,程序運行的起始地址一 般不可能是RAM。RAM在掉電之後內容會丟失,因此係統上電的時候,RAM中一般不會有有效的程序。但是在程序的調試階段,可以將程序直接載入RAM, 然後在RAM的程序載入地址處運行程序。
嵌入式系統RAM中的調試程序的內存佈局如圖13-6所示。
圖13-6 RAM中的調試程序的內存佈局
這是一種相對簡單的形式,因爲代碼段的存儲地址和運行地址是相同的,都是RAM(SDRAM或者SRAM)中的地址。在這種情況下,程序沒有運行初始化階段加載的問題。
從主機向目標機載入程序的時候,程序映像文件中代碼段(code或text)、只讀數據段、讀寫數據段依次載入目標系統RAM(SDRAM或者SRAM)的空間中。
程序載入到目標機之後,將從代碼區的地址開始運行,在運行的初始化階段,將開闢未初始化數據區,並將其初始化爲0,在運行時將動態開闢堆區和棧區。
在沒有操作系統的情況下,開闢內存的工作都是由編譯器生成的代碼完成的,實現的原理是在映像文件中加入這些代 碼。主要工作包括:在程序運行時根據實際大小開闢未初始化的數據段;初始化棧區的指針,這個指針和物理內存的實際大小有關;在調用相關函數 (malloc、free)時使用堆區,這些函數一般由調用庫函數實現。表13-4列出了C語言程序在RAM中的調試過程。
表13-4 C語言程序在RAM中的調試過程
階段 |
涉及的部分 |
主要工作 |
程序的映像 |
代碼段(Code) 只讀數據段(RO Data) 讀寫數據段(RW Data) |
將程序放置在RAM中 |
初始化階段 |
未初始化數據段(BSS) |
開闢BSS段並且清零 |
運行階段 |
代碼段(Code) 只讀數據段(RO Data) 讀寫數據段(RW Data) 未初始化數據段(BSS) 堆(heap) 棧(stack) |
運行RAM代碼段中的程序,動態地在RAM中開闢堆和棧 |
知識點:程序直接載入RAM運行時,程序的加載位置和運行位置是一致的,因此不存在段複製的問題,需要在初始化階段開闢未初始化區域,在運行時使用堆棧。
13.4.2 固化程序的XIP運行
固化應用是一種嵌入式系統常用的運行方式,其前提是目標代碼位於目標系統ROM(Flash)中。ROM中的區域包括映像文件的代碼段(code或text)、只讀數據段(RO Data)、讀寫數據段(RW Data)。
以XIP(在位置執行)方式運行程序時內存佈局如圖13-7所示。
代碼的運行也是在ROM(Flash)中,因此,在編譯過程中代碼的存儲地址和運行地址是相同的,由於上電時需要啓動,因此該代碼的位置一般是(0x0)。
在這種應用中,一件重要的事情就是將已初始化讀寫段的數據從Flash中複製到SDRAM中,由於已初始化讀寫段既需要固化,也需要在運行時修改,因此這一步是必須有的,在程序的初始化階段需要完成這一步。
圖13-7 XIP運行程序時的內存佈局
一般來說,在編譯過程中需要定義讀寫段和未初始化段的地址。在程序中可獲取這些地址,然後就可以在程序的中加入複製的代碼,實現讀寫段的轉移。表13-5列出了C語言程序的XIP運行過程。
表13-5 C語言程序的XIP運行過程
階 段 |
涉及的部分 |
主要工作 |
程序的映像 |
代碼段(Code) 只讀數據段(RO Data) 讀寫數據段(RW Data) |
程序放置在Flash中 |
初始化階段 |
讀寫數據段(RW Data) 未初始化數據段(BSS) |
複製讀寫數據段到RAM中 開闢未初始化段並且清零 |
運行階段 |
代碼段(Code) 只讀數據段(RO Data) 讀寫數據段(RW Data) 未初始化數據段(BSS) 堆(heap) 棧(stack) |
運行Flash代碼段中的程序,動態地在RAM中開闢堆和棧 |
知識點:程序在ROM或者Flash中以XIP形式運行的時候,不需要複製代碼段和只讀數據段,但是需要在RAM中複製讀寫數據段,並另闢未初始化數據段。
13.4.3 固化程序的加載運行
在某些時候,在存放程序的位置是不能運行程序的,例如程序存儲在不能以XIP方式運行的Nand-Flash或者硬盤中,在這種情況下,必須將程序完全加載到RAM中才可以運行。固化程序加載運行的內存佈局如圖13-8所示:
圖13-8 固化程序加載運行的內存佈局
依照這種方式運行程序,需要將Flash中所有的內容全部複製到SDRAM或者SRAM中。在一般情況 下,SDRAM或者SRAM的速度要快於Flash。這樣做的另外一個好處是可以加快程序的運行速度。也就是說,即使Flash可以運行程序,將程序加載 到RAM中運行也還有一定的優勢。
這樣做也產生了另外一個問題:代碼段的載入地址和運行地址是不相同的,載入地址是在ROM(Flash)中,但是運行的地址是在RAM(SDRAM或者SRAM)中。對於這個問題,不同的系統在加載程序的時候有不同的解決方式。
C語言固化程序的加載運行過程如表13-6所示。
表13-6 C語言固化程序的加載運行時各段的情況
階 段 |
涉及的部分 |
主要工作 |
代碼的映像 |
代碼段(Code) 只讀數據段(RO Data) 讀寫數據段(RW Data) |
將程序放置在Flash中 |
初始化階段 |
代碼段(Code) 只讀數據段(RO Data) 讀寫數據段(RW Data) 未初始化數據段(BSS) |
加載代碼段和只讀數據段到RAM中 複製讀寫數據段到RAM中 開闢未初始化段並且清零 |
運行階段 |
代碼段(Code) 只讀數據段(RO Data) 讀寫數據段(RW Data) 未初始化數據段(BSS) 堆(heap) 棧(stack) |
運行RAM代碼段中的程序,動態地在RAM中開闢堆和棧 |
知識點:固化程序在加載運行時,需要複製代碼段、只讀數據段和讀寫數據段到RAM中,並另闢未初始化數據段,然後在RAM中運行程序(執行代碼段)。
以這種加載方式的運行程序,另外一個重要的問題 是:如何把代碼移到RAM中。在有操作系統的情況下,代碼的複製工作是由操作系統完成的,在沒有操作系統的情況下,處理方式相對複雜,程序需要自我複製。 顯然,這種方式實現的前提是代碼最初放置在可以以XIP方式執行的內存中。
程序本身複製的過程也是需要通過程序代碼完成 的,這時需要程序中的代碼根據將包含自己的程序從ROM或者Flash中複製到RAM中。這是一個比較複雜的過程,程序的最前面部分是具有複製功能的代 碼。系統上電後,從ROM或者Flash起始地址運行,具有複製功能的代碼將全部代碼段和其他需要複製的部分複製到RAM中,然後跳轉到RAM中重新運行 程序。
固化程序加載複製和跳轉過程如圖13-9所示。
圖13-9 固化程序加載複製和跳轉過程
在代碼的前面一小部分是初始化的內容,這部分內容中有一部分是複製程序,這段複製程序將代碼段複製至RAM中,當這段初始化程序運行完成後,將跳轉到RAM中的某個地址運行。
13.4.4 C語言程序的運行總結
在上面幾節,主要介紹了C語言運行時內存的使用 情況。其關注點是程序中主要的段,事實上,程序可能不僅包括了上述主要段,還可能包括一些頭信息。程序實際的運行也分爲在操作系統下運行和直接運行等情 況。在具有操作系統的情況下,程序由操作系統加載運行,加載的時候可執行程序可以是一個文件,這個文件將包含程序的主要段以及頭信息。
對於Linux操作系統,目標程序是可執行的ELF(Executable and linking Format)格式;對於uCLinux操作系統,目標程序是Flat格式;對於需要在系統直接運行的程序,目標程序應該是純粹的二進制代碼,載入系統後,直接轉到代碼區地址執行。
事實上,無論運行環境如何,C語言程序在運行時所進行的動作都是類似的。程序在準備開始運行的時候,以下幾個條件都是必不可少的:
1.代碼段必須位於可運行的存儲區。
2.讀寫數據段必須在可以讀寫的內存中,而且必須經過初始化。
3.未初始化數據段必須在可以讀寫的內存中開闢,並被清空。
對於第1點,代碼段如果位於可以運行的存儲區域中(例如Nor Flash或者RAM),它就不需要加載,可以直接運行;如果代碼段位於不能運行的存儲區域中(例如:Nand Flash或者硬盤)中,它就必須被加載到RAM運行。