《深入BREW開發》——第三章 編譯器基礎

第三章 編譯器基礎
       看了這個題目,請不要誤會我要告訴您編譯器是怎麼實現的,我寫這節的主要目的是告訴您通常編譯器是怎樣對待您所寫的程序的。大家都知道,程序最終都要在CPU上運行,那麼像C語言這樣的高級語言來說,編譯器就是聯結C和CPU之間的橋樑了。在這一節裏我要告訴您編譯器是如何充當這個“橋樑”角色的。
       本節首先講述一下程序員的層次問題,目的是讓我們自己知道需要成爲一名什麼樣的程序員;然後分別是編譯器的相關介紹以及編譯器是如何“對待”程序等內容。
3.1 軟件和程序員的層次
       如果要了解程序員的層次,那就要先看看程序的層次了。看圖3.1:
程序源代碼
編譯器
中央處理器(CPU)


圖3.1 程序的層次
上圖的語言描述就是程序源代碼經過編譯器生成可以在CPU運行的二進制代碼。由此衍生出了程序員的兩個層次——語言層次和二進制層次。
所謂的語言層次的程序員,就是那些可以使用某一個編程語言的程序開發人員。大多數的初學者都屬於這個層次,能夠遵照一定的規範完成編碼工作,但是對下面的事情(編譯器和CPU)就一知半解了。
所謂的二進制層次的程序員,就是那些熟練地掌握某一個編程語言並且知道這個語言的來龍去脈的程序開發人員。他們對程序有着深刻的認識,對於程序中每一部份發生的事情瞭如指掌。二進制層次的程序員要比語言層次的高,是軟件中的高手,因此也比較難得。對於二進制層次的程序員來說,可以非常快速的掌握一門開發語言,因爲對於他們來說在二進制層次上只有CPU指令的差別而沒有語言的差別。不過要想成爲一名這樣的程序員是需要付出很多努力,並積累足夠的編程經驗纔可以。
程序語言所要解決的問題就是如何更加高效的組織二進制代碼。例如,彙編語言是對CPU指令的符號化,目的是爲了容易使用CPU指令從而提高編碼效率。由於彙編語言對二進制代碼基本沒什麼組織,因此稱它爲“低級語言”。C/C++語言對二進制代碼的組織有了很大的提高,制定了完全脫離了CPU指令的語法規則,大大提高了程序開發效率,因此我們稱它爲“高級語言”。從這個角度來講,二進制始終是任何程序開發語言的歸宿,因此在二進制層次理解程序是十分必要的。
然而,正是這些高級語言制定了脫離二進制指令的語法規則,將大部分二進制細節都隱藏在了編譯器之內,因此加大了程序員與二進制之間的距離,使得成爲一名二進制層次的程序員更加困難了。在這一節裏我要向您揭示那些隱藏在編譯器裏的二進制細節,期望能夠爲您成爲一名二進制層次的程序員提供幫助。
3.2 編譯器的分類和作用
       從不同的角度可以進行不同的編譯器分類,因此在這裏列舉一些典型的編譯器並一一進行講解,這樣我們就可以更加直觀的看到各個編譯器之間的不同部分。這些典型的編譯器分別是:彙編語言編譯器、Borland C/C++編譯器、ARM C/C++編譯器、VC++編譯器和典型的Java編譯器。由於本書主要使用C語言相關的工具,因此這裏主要列舉的是C語言相關編譯器。
       還有重要的一點是,在本書中大部分的編譯器指的是編譯器和鏈接器,除非它們兩個擺在一起。
3.2.1彙編語言編譯器
       我們知道,彙編語言只是簡單的對CPU指令進行了封裝(封裝也就是包裝的意思,通常計算機術語裏這麼稱呼),因此彙編語言編譯器是與CPU相關的。結果是使用彙編語言編寫代碼的可移植性很差,因爲不同的CPU可能指令系統和寄存器都不一樣。瞭解計算機歷史的人應該知道,最初寫的程序都是二進制的,就像是最原始的打孔機之類的東東,需要每個程序員記住每個指令的二進制值(也就是0和1的組合),編程效率可想而知(向那一代程序員致敬!)。不過彙編語言的出現使得編程效率大大的得到提升,程序員再也不需要記憶指令的二進制值了。也是從這裏開始,程序效率提升的步伐大大加快了。緊接着,高級語言和計算機操作系統的飛躍發展更爲提高編碼效率創造了良好的條件。
3.2.2 Borland C/C++編譯器
       這個編譯器是在DOS操作系統時代非常流行的C語言編譯器。從這句描述可以看出它是與操作系統相關的,當然要完成在CPU上運行是一定要和CPU相關的。通常對於一個編譯器來說會根據不同的CPU類型生成不同的二進制代碼,這通常通過爲編譯器傳遞參數來實現。
       現在可能有些人會問一個雞生蛋還是蛋生雞的問題:編譯器是由誰來編譯的?如果一定要追根溯源的話,我會告訴您,世界上第一個彙編語言編譯器是用機器碼寫的,因此不需要編譯;世界上第一個高級語言編譯器是由彙編語言寫的,操作系統也在這個過程中不斷的發展。也正是由於不同CPU和操作系統平臺以及編譯器的交互發展,才產生了當今世界如此的軟件規模。也可以想象的到英特爾和微軟等歐美企業屹立不倒的原因了,因爲他們伴隨着計算機產業一同成長了那麼多年。
3.2.3 ARM C/C++編譯器
       ARM是當前嵌入式系統中最爲常用的CPU內核芯片解決方案。由於ARM主要用於嵌入式系統,且嵌入式系統的操作系統千差萬別,因此它的目標環境要求與操作系統無關,甚至於操作系統的源代碼也是與應用程序代碼一起編譯的。因此從這個意義上來說,嵌入式系統對於二進制的表現是最完全的,從中我們可以一窺整個系統二進制層次的原貌。在以後的章節中我們也將主要圍繞着ARM編譯器進行講解。
3.2.4 VC++編譯器
       VC++編譯器是微軟的基於Windows操作系統的C/C++編譯器,它是緊密的與Windows操作系統相關的。也正是由於它和Windows操作系統聯繫之緊密,導致它與跨平臺沒有了任何的關係。如果說對於普通的編譯器,如ARM C/C++編譯器、Borland C/C++,由於其需要實現的平臺相關代碼較少,我們可以比較容易的實現跨平臺無關的代碼開發,那麼,VC++的編譯器就十分的困難了,基本上不可能。
3.2.5 Java編譯器
       Java是號稱跨平臺的,沒錯,確實是這樣子。Java能夠實現跨平臺的前提條件是需要在這個Java平臺上實現Java虛擬機,這個虛擬機可就不是跨平臺的啦,而是與平臺緊密相關的,每一種平臺要實現各自的Java虛擬機。由此理解,Java編譯器編譯的目標代碼不是針對CPU和操作系統的,而是真對Java虛擬機的。所以也可以稱Java語言是一種半解釋型的語言。從這裏還可以看出,Java程序的執行效率比直接編譯生成二進制的程序執行效率要低。
      
       接下來看看上面的各種編譯器與CPU和操作系統的關係。看下錶:
名稱 與CPU相關性 與操作系統相關性
彙編語言編譯器 強相關 無關
Borland C/C++編譯器 強相關 相關
ARM C/C++編譯器 強相關 無關
VC++編譯器 強相關 強相關
Java編譯器 無關 無關

       爲了我們知識的完整性,我想還是有必要提及一下腳本(Script)語言。腳本語言廣泛的應用於網頁設計中,嵌入到網頁內部實現動態或交互的Web應用。典型的是JavaScript和VBScript。腳本語言屬於完全的解釋型語言,不需要編譯器,直接由腳本的處理程序生成結果。
3.3 編譯器的數據處理方式
       編譯器的作用是將源代碼編譯成可以在目標平臺運行的程序,核心要處理的就是程序員所寫的程序。因此,對於編譯器要處理的主要程序元素有:代碼、局部變量、全局變量、靜態變量以及常量,概括起來就是代碼和數據。如何在二進制層面組織代碼和數據就是編譯器需要完成的工作。通常在二進制層面存在以下三種類型的二進制數據:
       1、只讀數據(RO Read Only):程序和常量
       2、可讀可寫數據(RW Read &Write):有初始化值的全局變量和靜態變量
       3、零初始化數據(ZI Zero Initialize):無初始化值的全局變量和靜態變量
       最終這些數據將會通過編譯連接生成一個二進制的可執行文件,並將這些數據分別放在不同的可執行文件段(Section)中:只讀數據放在text段中;可讀可寫數據放在data段中;另初始化數據放在bss(Block Started by Symbols)段中。接下來我們將看一看這些數據究竟是如何組織的。
3.3.1只讀數據
       只讀數據是我們的程序在執行的過程中不能修改的數據,開動腦筋,仔細地想一想在我們的程序中那些數據是不能修改的呢?
       第一個出現在我們腦海中的應該是那些常量。在C語言中,我們可以使用const關鍵字來聲明一個常量。我們知道,一個變量按照其作用域可以分爲全局變量和局部變量,對於一個全局的const變量來說,屬於只讀的數據是沒有任何疑義的。但是對於一個使用const關鍵字的局部變量來說就有問題了,這些變量由於屬於局部變量,因此在運行的時候仍然是放在堆棧中的。如果希望它變成無可爭議的只讀數據的話,那麼我們需要同時聲明這個變量爲static類型的,因爲在C語言中,靜態變量採用了與全局變量一樣的處理方式。
       除了常量之外,第二個屬於只讀數據的就是代碼本身。代碼是數據嗎,有沒有搞錯?不要心存疑問,事實的確如此!在編譯器看來,不論時變量還是代碼,都屬於數據。恰恰是程序中的代碼是隻讀數據的主力軍。因爲在運行的時候,代碼是不可改變的,所以對編譯器來說,代碼屬於只讀數據來說就不奇怪了。
3.3.2可讀可寫數據
       可讀可寫數據是指那些在代碼中指定了初始化變量的全局變量和靜態變量。在C語言中,全局變量屬於固定分配內存的方式,需要在鏈接的時候就爲其分配固定的存儲空間。這些存儲空間屬於這個變量私有,從系統啓動到關閉爲止都只能由這個變量使用。如果一個非常量的全局變量含有初始值,那麼我們就需要首先存儲這些初始值,並把它們保存在我們生成的二進制可執行文件中,同時爲它分配一個RAM中固定的存儲空間。當我們開始執行這個可執行文件的時候,需要將這些初始值從可執行文件中複製到它所對應那段固定存儲空間中。
       從某種程度上來說,可讀可寫的數據中的一步分需要存儲在可執行文件的text區中,因爲可讀可寫變量的初始值要存儲在一個只讀的空間中,在運行的時候纔會複製到該變量對應的空間中。而這部分的初始值也就是隻讀數據的一步分了。這一點在不支持虛擬內存管理的嵌入式系統中表現得尤爲突出。在採用這種方式的系統中是十分佔用系統存儲空間的,因爲它既要佔用代碼段來存儲初始值,而且也要佔用同等的RAM空間來存儲數據。
3.3.3零初始化數據
       零初始化數據就是在程序中定義的那些沒有初始值的全局變量和靜態變量。這些變量的特點就是固定分配存儲空間,使用0做爲初始化的值。這樣,整個程序中零初始化變量就可以連接成爲一個整片的內存區域,只需要知道地址區間,然後在初始化的時候都賦值爲0就可以了。在執行程序的時候,展開ZI數據的信息,根據地址區間全部進行零初始化賦值操作。因此,零初始化數據之需要佔用很少的空間來記錄起始地址和結束地址就可以了。
       在嵌入式系統中,由於操作系統的代碼和數據也包含在這三種類型的數據裏面,因此對這三種數據的處理方式就有了很大的不同。從前面的章節中我們知道,嵌入式系統主要有CPU+ RAM+ Flash的結構,因此對於系統中的只讀數據是存儲在Flash中的,而RW和ZI數據則是存儲在RAM中的。Flash芯片中存儲的就相當於在Windows操作系統下的可執行文件(.exe),在系統啓動的時候,將存儲在Flash中的RW數據複製到對應的內存區域中,將ZI數據按照起始和結束地址零初始化相應的區域。
       到現在爲止,細心的讀者可能會發現還沒有提及局部變量的處理過程,他們是存儲在什麼地方的?首先說明這裏所說的局部變量不包含局部的靜態變量,因爲不管是全局靜態變量還是局部靜態變量全部按照全局變量方式處理。局部變量產生於函數的內部,因此它的產生和消亡也都在函數裏面。在程序開始執行某一函數的時候,會爲這個函數內聲明的局部變量在棧內分配空間,在函數結束的時候將這些空間彈出。由此可以看出,在使用局部變量的時候要考慮棧的空間大小。由於系統中的棧主要由操作系統來管理,並且通常分配的空間是有限的(尤其是在嵌入式系統中),因此不要在函數體內使用較大的數組。
3.4 編譯和鏈接
       當我們行進到這裏的時候,我想我們不得不先澄清一下編譯器和鏈接器的概念。在前面討論的編譯器中是包含了編譯器和鏈接器兩個部分的,用它來代表從代碼到可執行程序的全部處理過程。在實際的編譯生成可執行程序中實際上包含了兩個步驟——編譯和鏈接。編譯的作用是執行預編譯操作、檢查源程序中的錯誤以及生成中間目標碼,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File;鏈接的作用是將編譯過程產生的中間目標碼(Object File)組合生成最終的二進制可執行文件。這主要是考慮到有多個源文件的情況下可以單獨的編譯每一個源文件(或者可以稱作模塊),這樣就爲大工程的源代碼編譯管理帶來了極大的方便,關於工程管理的知識我將在工程管理基礎部分講解。
       編譯時,編譯器需要的是語法的正確、函數與變量的聲明的正確、編譯器頭文件的所在位置的正確,只要這些內容都正確,編譯器就可以編譯出中間目標文件。一般來說,每個源文件(.c/.cpp)都應該對應於一箇中間目標文件(O文件或是OBJ文件)。
鏈接時,主要是鏈接函數和全局變量,所以,我們可以使用這些中間目標文件(O文件或是OBJ文件)來鏈接我們的應用程序。鏈接器並不管函數所在的源文件,只管函數的中間目標文件(Object File)。鏈接完成後就生成了可以運行的二進制可執行文件。
3.5 控制可執行程序的生成
       在完成了編譯和鏈接生成可執行程序之後,我們或許要問怎麼樣控制這個可執行程序的生成呢?是編譯器全部都安排好了嗎?如果能夠控制可執行程序的生成,那麼採用什麼方式控制呢?
       這一切的控制都是由鏈接器完成的。任何一個鏈接器都會有相應的方式控制程序的入口點(Entry)、程序基址(也就是程序在內存中的第一個位置)等內容。在VC等IDE開發環境中,已經通過“工程屬性”的鏈接器設置選項來控制這些內容。典型的嵌入式系統中ARM編譯器是通過一個叫做Scatter-Loading的.scl描述文件來控制整個二進制可執行程序的存儲器安排。在我們前面的程序世界中我們都是使用main()函數做爲C語言的程序入口,但實際上我們可以通過編譯器的選項來控制這個入口函數。比如,我們可以在VC工程中的工程屬性的編譯器選項中指定入口點是mymain()函數,而不是main()函數。在ARM鏈接器中通過-first來指定入口函數的值。
       爲了能夠讓我們更加深入的理解二進制可執行文件,在這裏介紹一下ARM編譯器使用的Scatter-Loading機制。首先我們先來看一個真實的Scatter-Loading描述文件的例子,這個例子的全部文件在Test4文件夾的system.scl文件內:
CODE_ROM 0 0xFFFFFF
{
       # Boot Block區域
    BB_ROM +0x0
    {
        bootblock.o (BOOTBLOCK_IVT, +FIRST)
        bootblock.o (BOOTBLOCK_CODE)
        bootblock.o (BOOTBLOCK_DATA)
    }
 
    # 主程序代碼和常量
    MAIN_APP +0x0
    {
        * (+RO)
    }
 
    # RAM數據
    APP_RAM 0x10000000
    {
        * (+RW, +ZI)
    }
 
    # 用戶自定義的保存區域
    RESERVED_RAM 0x10700000
    {
        mainapp.o (reserved)
    }
}

在這個文件中,CODE_ROM 0 0xFFFFFF指定了這個最終生成的二進制文件地址空間空間在0-0xFFFFFF內,也就是16M字節空間。在這個內部,依次規劃了Boot Block區域的代碼和常量數據段(RO數據)、主應用程序的代碼和常量數據段(RO數據)、應用程序RAM數據(RW和ZI數據)以及用戶特殊用途的保留區域的數據。
BB_ROM +0x0中的+0x0表示緊接着上面分配的空間,這裏就是0。MAIN_APP +0x0就表示了接着BB_ROM之後的空間進行分配。APP_RAM 0x10000000表示RAM的地址空間從0x10000000地址開始,依次類推,這個內存影射的示意圖如下:
Boot Block(ROM Code & Const)
應用程序(ROM Code & Const)
應用程序(RAM ZI& RW)
 
用戶自定義保留區(RAM reserved)
0x00000000
Unknown Address
0x10000000
Unknown Address
0x10700000


圖3.2存儲器映射圖
       用戶保留區域是由用戶根據需要通過#pragma arm section指定的數據區域類型,可以參看Test4中的mainapp.c的定義。
bootblock.o (BOOTBLOCK_IVT, +FIRST)是非常關鍵的一句,它描述了將bootblock.o中的BOOTBLOCK_IVT區域的數據做爲最前端數據,+FIRST表示放在這個地址段的最前面,在這裏也就是ARM CPU的中斷向量表區域。
bootblock.o (BOOTBLOCK_CODE)和bootblock.o (BOOTBLOCK_DATA)中說明的是使用BOOTBLOCK_CODE 和BOOTBLOCK_DATA 說明的區域,可以參看bootblock.s中的相關內容。
* (+RO)表示將所有尚未匹配的只讀數據放在這個區域,* (+RW, +ZI)表示將尚未匹配的RW和ZI數據放在這個空間中。
mainapp.o (reserved)表示將mainapp.o中的使用“reserved”包含的數據放在這個空間中。
       整個Test4工程展示了一個完整的ARM系統的構建要素,請認真地研究這個例子,它一定能夠讓您更加深刻的認識一個嵌入式系統。並且也有助於我們理解其他的系統,畢竟所有的系統原理都是相通的。在工程管理(Make File)一節中還將詳細的展示怎樣使用這個.scl文件。
3.6 可執行程序的啓動過程
       對於一直生活在Windows世界的人們來說,最熟悉的可執行程序莫過於.exe文件了。每當打開這些文件的時候,都會啓動一個應用程序。對於寫程序的人來說,還知道程序中的main()函數或者由鏈接器指定的入口函數是首先被執行的。這種說法是正確的,只不過忽略了執行入口函數前的處理。最簡單的,具有初始化值的全局變量是優先於main函數被賦值的。爲了說明這個問題我們還是來看看Test4中的原文件吧。這個例子裏面購建了一個相對完整的從ARM啓動到執行main函數的過程,相信會對您理解其他的程序也會有所幫助。如果您有make運行環境和ADS1.2環境的話,這些代碼是可以編譯鏈接的。
與前面簡短的例子不同,這個例子中包含了三個原文件和一個頭文件,總共約300行程序。我相信,對於一個程序員來說,300行程序也不算什麼。在這裏我們佔用了8頁多的篇幅,相信這裏是本書中最長的一段代碼了,不過我覺得有必要講解這段程序,好讓我們看一看以前從未重點關心過的部分。
system.h – 系統頭文件
#ifndef SYSTEM_H
#define SYSTEM_H
/*====================================================================
                        ARM 處理器常量定義
文件名:system.h
描述:
    這個文件內定義了ARM處理器的常量和棧尺寸,以及一個彙編的調用函數的宏
====================================================================*/
#ifndef _ARM_ASM_
typedef unsigned char     byte;         /* Unsigned 8 bit value type. */
typedef unsigned short    word;         /* Unsinged 16 bit value type. */
typedef unsigned long     dword;        /* Unsigned 32 bit value type. */
#endif //_ARM_ASM_
 
/* CPSR Control Masks         */
#define PSR_Fiq_Mask         0x40
#define PSR_Irq_Mask         0x80
 
/* Processor mode definitions */
#define PSR_Supervisor       0x13
#define PSR_System           0x1f
 
/* Stack sizes.               */
#define SVC_Stack_Size       0xd0
#define Sys_Stack_Size       0x400
 
/* 用戶數據區大小             */
#define USER_HEAP_SIZE       0x1000
 
#if defined(_ARM_ASM_)
/*====================================================================
 名稱: blatox
 描述:
      不必理會是在ARM或THUMB模式下調用函數的方法
 Arguments:
    destreg - 包含被叫函數地址的寄存器
 被修改的寄存器: lr
====================================================================*/
        MACRO
        blatox     $destreg
        ROUT
 
        tst     $destreg, #0x01         /* Test for thumb mode call. */
 
        ldrne   lr, =%1
        ldreq   lr, =%2
        bx      $destreg
1
        CODE16
        bx      pc
        ALIGN
        CODE32
2
        MEND
#endif // _ARM_ASM_
#endif //SYSTEM_H

 
bootblock.s – 系統入口文件
;*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
;                  System Boot Block
; 文件名:     bootblock.s
; 描述:  
; 此模塊中定義了系統的中斷向量表和Reset時的處理函數
;*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
;=====================================================================
;                           MODULE INCLUDE FILES
;=====================================================================
#include "system.h"
 
;=====================================================================
;                             MODULE IMPORTS
;=====================================================================
        IMPORT ram_init
        IMPORT __rt_entry
       
        ; 導入系統棧的地址
        IMPORT svc_stack
        IMPORT sys_stack
       
        ; 導入由鏈接器根據Scatter-Loading描述文件生成的數據區域RAM地址
        ; 和數據區大小
       
        ; 應用程序RAM
        IMPORT |Load$$APP_RAM$$Base|     
        IMPORT |Image$$APP_RAM$$Base|
        IMPORT |Image$$APP_RAM$$Length|    
        IMPORT |Image$$APP_RAM$$ZI$$Base|
        IMPORT |Image$$APP_RAM$$ZI$$Length|
       
        ; 用戶保留區RAM
        IMPORT |Load$$RESERVED_RAM$$Base|
        IMPORT |Image$$RESERVED_RAM$$Base|
        IMPORT |Image$$RESERVED_RAM$$Length|
        IMPORT |Image$$RESERVED_RAM$$ZI$$Base|
        IMPORT |Image$$RESERVED_RAM$$ZI$$Length|
       
;=====================================================================
;                             MODULE EXPORTS
;=====================================================================
        ; 導出重新命名過的鏈接器符號,以便於其他模塊使用
       
        ; 應用程序RAM
        EXPORT Load__APP_RAM__Base
        EXPORT Image__APP_RAM__Base
        EXPORT Image__APP_RAM__Length
        EXPORT Image__APP_RAM__ZI__Base
        EXPORT Image__APP_RAM__ZI__Length
       
        ; 用戶保留區RAM
        EXPORT Load__RESERVED_RAM__Base
        EXPORT Image__RESERVED_RAM__Base
        EXPORT Image__RESERVED_RAM__Length
        EXPORT Image__RESERVED_RAM__ZI__Base
        EXPORT Image__RESERVED_RAM__ZI__Length
       
        ; 輸出__main 和 _main 符號避免鏈接器包含標準C運行庫的初始化處理
 
        EXPORT __main
        EXPORT _main
;=====================================================================
;                      處理器中斷向量表
; ARM處理器的中斷向量表開始於0x00000000H地址,這裏也是系統重置時的入口點
;
; 除了系統重置以外,其餘所有的中斷都應該引入到異常處理程序中,但是這裏沒
; 有這樣處理,僅提供了簡單的示例,並統一調用boot_reset_handler進入重置程
; 序
;=====================================================================
        AREA    BOOTBLOCK_IVT, CODE, READONLY
        CODE32                     ; 32 bit ARM instruction set.
__main
_main
        ENTRY                      ; Entry point for boot image.
       
        ;ARM CPU中斷向量表
        b       boot_reset_handler     ; ARM reset
        b       boot_reset_handler     ; ARM undefined instruction interrupt
        b       boot_reset_handler     ; ARM software interrupt
        b       boot_reset_handler     ; ARM prefetch abort interrupt
        b       boot_reset_handler     ; ARM data abort interrupt
        b       boot_reset_handler     ; Reserved by ARM Ltd.
        b       boot_reset_handler     ; ARM IRQ interrupt
        b       boot_reset_handler     ; ARM FIQ interrupt
       
;====================================================================
; 函數名:boot_reset_handler
; 描述:
; 系統初始化函數,進行如下處理:
;    1. 初始化系統棧
;    2. 初始化RAM
;    3. 調用__rt_entry初始化C語言庫,並調用C語言的main函數
;=====================================================================
        AREA BOOTBLOCK_CODE, CODE
        CODE32                  ; 32 bit ARM instruction set
       
boot_reset_handler
        ; 進入超級用戶模式並安裝超級用戶模式下的棧
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
        ldr     r13, =svc_stack+SVC_Stack_Size
       
        msr     CPSR_c, #PSR_System:OR:PSR_Fiq_Mask:OR:PSR_Irq_Mask
        ldr     r13, =sys_stack+Sys_Stack_Size
       
        // 返回超級用戶模式
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
       
        ; 初始化RAM
        ldr     r4, =ram_init
        blatox r4
       
        ldr     a3, =__rt_entry
        bx      a3
       
        AREA    BOOTBLOCK_DATA, DATA, READONLY
 
;=====================================================================
;                       BOOT BLOCK 數據地址
; 根據導入的鏈接器RAM數據區地址,重新生成新的符號給Boot RAM初始化程序使用。
; 由於RAM初始化程序使用C語言完成,而C編譯器需要-pcc參數才能使用$符號,因此
; 這裏做一個符號的變換使得C語言可以直接使用
;
;=====================================================================
; 應用程序RAM
Load__APP_RAM__Base
        DCD |Load$$APP_RAM$$Base|
                   
Image__APP_RAM__Base
        DCD |Image$$APP_RAM$$Base|
              
Image__APP_RAM__Length
        DCD |Image$$APP_RAM$$Length|
                   
Image__APP_RAM__ZI__Base
        DCD |Image$$APP_RAM$$ZI$$Base|
                   
Image__APP_RAM__ZI__Length
        DCD |Image$$APP_RAM$$ZI$$Length|
       
; 用戶保留區RAM
Load__RESERVED_RAM__Base
        DCD |Load$$RESERVED_RAM$$Base|
 
Image__RESERVED_RAM__Base
        DCD |Image$$RESERVED_RAM$$Base|
 
Image__RESERVED_RAM__Length
        DCD |Image$$RESERVED_RAM$$Length|
 
Image__RESERVED_RAM__ZI__Base
        DCD |Image$$RESERVED_RAM$$ZI$$Base|
 
Image__RESERVED_RAM__ZI__Length
        DCD |Image$$RESERVED_RAM$$ZI$$Length|
       
        END

 
raminit.c – RAM初始化和C運行庫替換函數
/*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
                        RAM初始化文件
 
文件名:raminit.c
描述:
    此模塊內包含了初始化RAM的函數以及鏈接時需要替換的C運行時庫函數
 
*====*====*====*====*====*====*====*====*====*====*====*====*====*====*/
/*====================================================================
                           Include Files
====================================================================*/
#include "system.h"
 
/*====================================================================
                           Global Data
====================================================================*/
 
/* 由於在RAM初始化之前還不能夠使用RAM,
    因此這裏使用CPU寄存器做爲變量的存儲空間*/
__global_reg(1) dword *dst32;
__global_reg(2) dword *src32;
__global_reg(3) dword *stop_point;
 
/* 應用程序RAM */
extern byte * Load__APP_RAM__Base;
extern byte * Image__APP_RAM__Base;
extern byte * Image__APP_RAM__Length;
extern byte * Image__APP_RAM__ZI__Base;
extern byte * Image__APP_RAM__ZI__Length;
 
/* 用戶保留區RAM */
extern byte * Load__RESERVED_RAM__Base;
extern byte * Image__RESERVED_RAM__Base;
extern byte * Image__RESERVED_RAM__Length;
extern byte * Image__RESERVED_RAM__ZI__Base;
extern byte * Image__RESERVED_RAM__ZI__Length;
 
/*====================================================================
                           Function Declare
====================================================================*/
/*CRT庫函數替換*/
void __user_initial_stackheap ( ) { return; }
 
/*====================================================================
函數名:ram_init
 
描述:
 初始化RAM
依賴文件
 None
返回值
 None
====================================================================*/
void ram_init(void)
{
 /* 複製應用程序區的已初始化數據 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__Base +
                           (dword) Image__APP_RAM__Length);
 for( src32 = (dword *) Load__APP_RAM__Base,
         dst32 = (dword *) Image__APP_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
 
 /* 初始化應用程序區的零初始化數據 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__ZI__Base +
                           (dword) Image__APP_RAM__ZI__Length);
 for( dst32=(dword *) Image__APP_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
   
 /* 複製用戶保留區的已初始化數據 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__Base +
                           (dword) Image__RESERVED_RAM__Length);
  for( src32 = (dword *) Load__RESERVED_RAM__Base,
         dst32 = (dword *) Image__RESERVED_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
   
 /* 初始化用戶保留區的零初始化數據 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__ZI__Base +
                           (dword) Image__RESERVED_RAM__ZI__Length);
 for( dst32=(dword *) Image__RESERVED_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
}

 
mainapp.c – 程序主函數
/*====*====*====*====*====*====*====*====*====*====*====*====*====*====*
                        主應用程序文件
 
文件名:mainapp.c
描述:
    此模塊內包含了Main主函數
 
*====*====*====*====*====*====*====*====*====*====*====*====*====*====*/
/*====================================================================
                           Include Files
====================================================================*/
#include "system.h"
 
/*====================================================================
                           Global Data
====================================================================*/
#pragma arm section zidata = "reserved"
static byte gUserHeap[USER_HEAP_SIZE];
#pragma arm section zidata
 
/*----------------------------------------------------------------------------
 超級用戶模式下的堆棧值
----------------------------------------------------------------------------*/
byte svc_stack[SVC_Stack_Size];
byte sys_stack[Sys_Stack_Size];
 
/*====================================================================
                           Function Declare
====================================================================*/
int mymain( void );
 
/*====================================================================
函數名:main
描述:
 程序運行的入口點
依賴文件
 None
返回值
 None, this routine does not return
====================================================================*/
int main( void )
{
    return mymain();
}
 
/*====================================================================
函數名:main
描述:
 程序運行的入口點
依賴文件
 None
返回值
 None, this routine does not return
====================================================================*/
int mymain( void )
{
    int i;
   
    // 初始化用戶Heap區域
    for(i=0; i<USER_HEAP_SIZE; i++)
    {
        gUserHeap[i] = 0xFF;
    }
   
    for(;;){}// 一直在此處循環執行
}

在system.h文件中定義了一些常量,和一個彙編宏blatox,這個宏的用途是無縫的在彙編和C語言中進行調用。之所以有這個宏是因爲C語言可以使用ARM的ARM指令集或THUMB精簡指令集,前者是32位的指令長度,後者是16位指令長度。16位指令集比32位指令集更加節省二進制代碼空間。由於在系統啓動的時候CPU使用的全部是ARM指令,因此,如果當前C語言編譯的代碼是THUMB指令的話就需要不同的處理方式,函數blatox就可以通過判斷寄存器的值來定位即將調用的代碼是THUMB還是ARM的。
bootblock.s是一個彙編語言文件,我們從中可以看見它使用了#include來包含system.h文件。如果我們直接使用匯編編譯器armasm是不能夠識別的,因此要首先使用armcc的C語言編譯器處理一下。指令如下:
armcc -ansic -E -cpu ARM7TDMI -apcs /noswst/interwork -D_ARM_ASM_ bootblock.s > bootblock..i
生成處理之後的bootblock.i純彙編文件然後才能使用armasm編譯。如果想了解其中每個參數的意義,可以查看ARM編譯器的幫助文檔。這裏僅說明-D的作用與#define相同。
下面的代碼是ARM CPU的中斷向量表,根據上一節的Scatter-Loading描述文件可以知道,這個表的起始地址是0x00000000,也就是CPU一上電就會在此處取得指令執行。0x00000000地址是一個跳轉指令,轉到boot_reset_handler代碼段執行。
AREA    BOOTBLOCK_IVT, CODE, READONLY
        CODE32                     ; 32 bit ARM instruction set.
__main
_main
        ENTRY                      ; Entry point for boot image.
       
        ;ARM CPU中斷向量表
        b       boot_reset_handler     ; ARM reset
        b       boot_reset_handler     ; ARM undefined instruction interrupt
        b       boot_reset_handler     ; ARM software interrupt
        b       boot_reset_handler     ; ARM prefetch abort interrupt
        b       boot_reset_handler     ; ARM data abort interrupt
        b       boot_reset_handler     ; Reserved by ARM Ltd.
        b       boot_reset_handler     ; ARM IRQ interrupt
        b       boot_reset_handler     ; ARM FIQ interrupt

       這段代碼中的__main和_main是爲了替換掉鏈接時C運行庫的相關內容的,各位讀者可以試着在Test4種去掉這段代碼,鏈接時將看到“Image does not have an entry point”的警告信息。
       緊接着程序跳轉到boot_reset_handler代碼段:
        AREA BOOTBLOCK_CODE, CODE
        CODE32                  ; 32 bit ARM instruction set
       
boot_reset_handler
        ; 進入超級用戶模式並安裝超級用戶模式下的棧
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
        ldr     r13, =svc_stack+SVC_Stack_Size
       
        msr     CPSR_c, #PSR_System:OR:PSR_Fiq_Mask:OR:PSR_Irq_Mask
        ldr     r13, =sys_stack+Sys_Stack_Size
       
        // 返回超級用戶模式
        msr     CPSR_c, #PSR_Supervisor:OR:PSR_Irq_Mask:OR:PSR_Fiq_Mask
       
        ; 初始化RAM
        ldr     r4, =ram_init
        blatox r4
       
        ldr     a3, =__rt_entry
        bx      a3
        

       在這段代碼中首先設置了系統的堆棧空間,然後是調用了ram_init函數執行RAM的初始化。再之後就調用C語言運行庫的入口__rt_entry函數,從這裏就正式進入了C的世界,當然在這裏如果我們不想使用任何C語言庫函數的話,我們可以指定一個這個值是mymain並且屏蔽掉mainapp.c中的main()函數。具體替換方法如下:
       1、註釋掉mainapp.c中的main()函數
       2、註釋掉bootblock.s文件24行的IMPORT __rt_entry語句,替換成IMPORT mymain語句
       3、將bootblock.s文件129行的__rt_entry替換成mymain,並使用blatox a3替換掉bx a3語句
       這個時候編譯鏈接的語句就是沒有任何C語言庫函數影響的純粹C語言版本的程序了。產生這種想法的動機是,在一個大型系統中希望能夠自己實現相關的庫函數或禁用編譯器的庫函數。例如,printf是輸出一個語句,但是不同平臺的輸出方式是不同的,在DOS操作系統中是輸出在顯示器上,而在手機上可能顯示在LCD屏幕上,或者進一步說,有些系統根本就沒有輸出。
       接下來看看ram_init中都作了些什麼:
void ram_init(void)
{
 /* 複製應用程序區的已初始化數據 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__Base +
                           (dword) Image__APP_RAM__Length);
 for( src32 = (dword *) Load__APP_RAM__Base,
         dst32 = (dword *) Image__APP_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
 
 /* 初始化應用程序區的零初始化數據 */
 stop_point = (dword *) ( (dword) Image__APP_RAM__ZI__Base +
                           (dword) Image__APP_RAM__ZI__Length);
 for( dst32=(dword *) Image__APP_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
   
 /* 複製用戶保留區的已初始化數據 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__Base +
                           (dword) Image__RESERVED_RAM__Length);
 for( src32 = (dword *) Load__RESERVED_RAM__Base,
         dst32 = (dword *) Image__RESERVED_RAM__Base;
       dst32 < stop_point;
       src32++, dst32++ )
    {
      *dst32 = *src32;
    }
   
 /* 初始化用戶保留區的零初始化數據 */
 stop_point = (dword *) ( (dword) Image__RESERVED_RAM__ZI__Base +
                           (dword) Image__RESERVED_RAM__ZI__Length);
 for( dst32=(dword *) Image__RESERVED_RAM__ZI__Base;
       dst32 < stop_point;
       dst32++ )
    {
      *dst32 = 0;
    }
}

       根據我們上面的Scatter-Loading描述文件,有兩段內存區域,一段是APP_RAM對應的鏈接器會給初始化程序提供如下幾個變量:
Load$$APP_RAM$$Base 初始化數據存儲的地址
Image$$APP_RAM$$Base 初始化數據RAM的基地址
Image$$APP_RAM$$Length 初始化數據的長度
Image$$APP_RAM$$ZI$$Base 零初始化數據RAM的基地址
Image$$APP_RAM$$ZI$$Length 零初始化數據的長度

再看這段程序初始化APP_RAM的過程,現在您應該可以看見Scatter-Loading描述文件與程序之間的關係了吧。這裏要說明的是,由於在ARM C編譯器中不能使用“$”符號,因此對於這些根據Scatter-Loading描述文件生成的鏈接符號我們在bootblock.s文件中進行了重新定義,使用“_”符號代替了“$”符號。
在這裏所初始化的內容是程序運行時的空間,這些空間裏的數據在關機之後是沒有任何信息保存下來的,不管是ZI還是RW的。也就是說,在每一次開機的過程中,所執行的操作是完全相同的。或許有人會問,如果我需要記錄一些變量的內容,讓它能夠在斷電後仍舊能夠保存下來,該怎麼做呢?非常不幸的告訴您,這裏不是實現這個功能的地方。這個功能需要文件系統或者其他非易失性(Non-Volatile NV)設備的支持,這通常就是一個系統存儲參數的地方,要區分系統中不同設備的不同作用。對於RESERVED_RAM的處理就和APP_RAM一樣了,就不再贅述了。這裏就是一個完整的系統啓動過程,進入了main函數之後,如何處理就是我們每個程序員自己的任務了。
在瞭解了ARM的啓動過程之後,您應該也能夠猜出在Windows下一個.exe可執行文件的執行過程了,雖然細節不同,但是思想是一樣的,我在這裏就不再贅述了。
3.7 函數的調用和返回
       在軟件基礎一節講解函數的時候,我們知道了每個函數在運行的時候會給參數們留“位子”,以便於在使用函數的時候可以通過這些“位子”讓函數輸出不同的處理結果。但是我們還可能對這些“位子”的組織方式和返回存在着疑問。這一節我將結合Windows下的VC編譯器和ARM編譯器來進行講解。當然不瞭解這些我們依然可以寫出程序,但是,既然我們要追根溯源,那麼還是瞭解一下吧。同時掌握這些東西還有利於優化程序。下表列出了幾種典型的函數調用約定:
調用方式 參數傳遞方式
VC cdecl 參數經堆棧進行傳遞,由調用者負責清除堆棧
VC stdcall 參數經堆棧進行傳遞,由函數本身負責清除堆棧
VC fastcall 2個dword或更小的參數通過寄存器傳遞,其餘經由堆棧傳遞。
ARM Call 4個dword或更小的參數通過寄存器傳遞,其餘經由堆棧傳遞。

       從上面的約定可以看出,不同的編譯器函數的調用約定細節是不一樣的,這也決定了不同調用約定的函數行爲的不同。
       cdecl調用約定由於由調用者負責堆棧的清除,因此可以實現可變參數的函數(如printf函數)。但是這樣也會增加程序的代碼空間,因爲每一個調用函數的地方都需要相關的堆棧處理代碼。
       stdcall調用約定是Windows操作系統API函數的默認調用方式,沒有進行什麼特別的處理。
       fastcall調用約定相當於stdcall約定的優化版本,它通過兩個寄存器來實現參數的傳遞,這樣就減少了堆棧的處理,因此可以加快函數的運行。同理可以推理出,如果fastcall調用約定的函數參數個數小於或等於2個的話,會取得最快的執行速度。
       ARM編譯器的調用約定是使用4個寄存器來傳遞參數,因此對於ARM平臺的程序來說,在小於等於4個參數的情況下可以得到最優化的函數調用效率。對於空間較大的參數(如一個大的結構體)ARM編譯器將通過堆棧進行傳遞。
       對於函數的返回值,如果需要的傳遞空間比較小則通過寄存器傳遞,如果需要的空間較大,則通過內存進行間接的傳遞。上述幾種方式的返回值傳遞都是一樣的。
3.8 開發中與運行時
       我一直希望能夠將您從C語言的世界中引入程序運行的二進制世界之中,並一直試圖將二者的內在聯繫展示給您。雖然我並不能準確地知道是否達到了目的,但是我還是想在這裏總結一下它們之間的關係。
       C語言與二進制分別對應了開發中與運行時兩個程序的過程概念。對於很多的初學者會問一些諸如CPU怎麼識別我定義的變量這類問題,回答這類問題是很棘手的。如果我告訴他CPU不認識我們定義的變量時,他一定會問CPU不認識那麼程序怎麼運行啊?這就像是一個在迷宮裏的人,迷失在開發中與運行時兩個世界組成的迷宮裏。
變量是開發中的概念,經過編譯器的處理,會將變量的外衣去掉,送進赤裸裸二進制數據的二進制世界中。編譯器就是這兩個世界的橋樑。在二進制世界裏,CPU只認識二進制的數據和指令,沒有數據類型的概念。例如,對於開發過程中的一個數組,到了二進制層面就變成了一個固定大小的二進制空間,不管這個數據類型是char的還是int的,CPU只是根據相應的操作指令來執行對這塊空間的操作。因此,對於在C語言中的諸如變量、函數、結構體以及數組等等,統統是開發過程中的概念,只有編譯器會接受這些概念。到了運行的時候CPU一概不認,CPU就是一個非常簡單的傢伙,只知道根據編譯器生成的二進制指令執行,“只要符合規範,我就一言不發”。
只有透徹的理解了開發中與運行時之間的關係,才能從深層次理解程序的運行。因此在某種程度上來說,理解一下編譯器本身的“內幕”是很有必要的。只有二進制世界,纔是程序真正的世界。
3.9 小結
       這一章中主要介紹了編譯器的分類以及編譯器在整個系統中的作用,同時也包含了一些程序運行的知識,在這裏着重說明了開發中和運行時兩個程序的階段,而編譯器則成爲了這兩個階段的橋樑。通過編譯器對開發中的代碼進行編譯鏈接,才生成了可以運行的代碼。編譯器的作用就是將源代碼轉換成可以在目標機器上運行的機器碼,這樣可以通過高級語言的開發大大提高機器碼的開發速度,可以說編譯器是一個提高開發效率的工具。
思考題
       編譯器將程序分成幾種類型的數據?不同類型之間的區別是什麼?
 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/Gemsea/archive/2007/01/26/1495202.aspx

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