ARM系統(tǒng)基本文件格式
這里所說的ARM系統(tǒng)基本文件格式,都是在基于ARM的嵌入式系統(tǒng)開發(fā)中常會(huì)碰到的文件格式。
ARM系統(tǒng)基本文件格式有三種:
1) BIN,平板式二進(jìn)制格式,一般用于直接燒寫到Flash中,也可以用于加載到monitor程序中。
2) ELF,EXECUTABLE AND LINKABLE FORMAT,一種通用的OBJECT文件格式,一般由GNU COMPILER COLLECTION (GCC)產(chǎn)生。
3) AXF,BIN格式的擴(kuò)展版,主體部分同BIN,在文件頭和尾加入了調(diào)試用的信息,用于AXD。
本文主要討論BIN與ELF。
首先說明,ELF格式是一種OBJECT文件格式。一般OBJECT文件都可以分成三類:可重定位OBJECT文件,可執(zhí)行OBJECT文件,共享OBJECT文件。ELF格式文件也可以分成這三種。
首先說說可重定位OBJECT文件。這種OBJECT文件一般由GCC中的ASSEMBLER(as)產(chǎn)生(請不要認(rèn)為GCC只是編譯器),里面除了二進(jìn)制的機(jī)器代碼,還有一些可用于進(jìn)行重定位的信息。它主要是作為LINKER(ld)的輸入,LINKER將跟據(jù)這些信息,將需要重定位的符號重定位,進(jìn)而產(chǎn)生可執(zhí)行的OBJECT文件。ELF格式的可重定位OBJECT文件由header與section組成。
Header 包括ELF header 與 section header. ELF header 位于文件的頭部,用于存儲(chǔ)目標(biāo)機(jī)器的架構(gòu),大小端配置,ELF header大小,object文件類型,section header 在文件中的偏移,section header 的大小,section header 中的項(xiàng)目數(shù)等信息。Section header 則定義了文件中每個(gè)section 的類型,位置,大小等信息。Linker就是通過查找ELF header,找到section header 的入口,再在section header 中找到相應(yīng)的section 入口,進(jìn)而定位到目標(biāo)section 的。
Section 包括
.text :經(jīng)過編譯的機(jī)器代碼。
.rodata :只讀的數(shù)據(jù),例如printf(“hello!”)中的字符串hello。
.data :已初始化的全局變量,局部變量將在運(yùn)行時(shí)被存放在堆棧中,不會(huì)在.data或 .bss段中出現(xiàn)。
.bss :未初始化的全局變量,在這里只是一個(gè)占位符,在object文件中并沒有實(shí)際的存儲(chǔ)空間。
.symtab :符號表,用于存放程序中被定義的或被引用到的全局變量和函數(shù)的信息。
.rel.text :一個(gè)保存著一系列在.text中的位置的列表。這些位置將在linker把這個(gè)文件與其它object文件合并時(shí)被修改,一般來說,這些位置都是保存著一些引用到全局變量或者外部函數(shù)的指令。引用局部變量或者本地函數(shù)的指令是不需要被修改的,因?yàn)榫植孔兞亢捅镜睾瘮?shù)的地址一般都是使用PC相對偏移地址的。需要注意的是,這個(gè)section 和下面的.rel.data在運(yùn)行時(shí)并不需要,生成可執(zhí)行的ELF object文件時(shí)會(huì)去掉這個(gè)section。
.rel.data :保存全局變量的重定位信息。一般來說,如果一個(gè)全局變量它的初始化值是另一個(gè)全局變量的地址,或者是外部函數(shù)的地址,那么它就需要被重定位。
.debug :保存debug信息。
.strtab : 一個(gè)字符串表,保存著.symtab和.debug ,和各個(gè)section的名字。.symtab,.debug 和section table里面,凡是保存name的域,其實(shí)都是保存了一個(gè)偏移值,通過這個(gè)偏移值在這個(gè)字符串表里面可以找到相應(yīng)得字符串。
下面仔細(xì)討論一下.symtab:
每一個(gè)可重定位的object文件,都會(huì)有一個(gè).symtab。這個(gè)符號表保存了在這個(gè)object文件中所有被定義的和被引用的符號。當(dāng)源程序是C 語言程序時(shí),.symtab 中的符號直接來源于C編譯器(cc1)。這里所說的符號主要有三種:
1) 在這個(gè)object文件中被定義的可以被其他object文件全局符號。在C語言源程序中,主要就是那些非靜態(tài)(沒有static 修飾的)的全局變量和非靜態(tài)的函數(shù)。在ARM匯編語言中,就是那些 被EXPORT 指令導(dǎo)出的變量。
2) 在這個(gè)object文件中引用到,但是在其他文件中定義的全局變量。在ARM匯編語言中就是通過IMPORT命令引入的變量
3) 本地變量。本地變量只在本object文件內(nèi)可見。這里的本地變量指的是連接器本地變量,應(yīng)該和一般的程序本地變量作區(qū)別。這里所指的本地變量,包括用static 修飾的全局變量,object文件中section名稱,源代碼文件名稱。一般意義上的本地變量,是在運(yùn)行時(shí)由系統(tǒng)的運(yùn)行時(shí)環(huán)境管理的,linker并不關(guān)心。
每個(gè)符合上面條件的符號在.symtab文件中都會(huì)有一個(gè)數(shù)據(jù)項(xiàng)。這個(gè)數(shù)據(jù)項(xiàng)的數(shù)據(jù)結(jié)構(gòu)是:
Typedef struct{
int name;//符號名稱,其實(shí)就是.strtab的偏移值
int value;//在section中的位置,以相對section地址的偏移表示
int size;//大小
char type;//類型,一般是數(shù)據(jù)或函數(shù)
char binding;//是本地變量還是全局變量
char reserved;//保留的位
char section;//符號所屬的section??蛇x有:.text(用數(shù)字1代表),.data(用數(shù)
//3代表),ABS(不應(yīng)被重定位的符號),UND(在本object文件
//中未定義的符號,可能在別的文件中定義),COM(一般的未初//始化的變量符號)。
}ELF_sym
現(xiàn)在假設(shè)組成應(yīng)用的各個(gè)模塊都已經(jīng)被匯編,構(gòu)建出了可重定位的object文件。這些object的結(jié)構(gòu)都是一樣的,有各自的.text, .data section, 有各自的.symtab. GCC下一步要做的就是使用linker (ld),把這些object文件,加上必要的庫連接成具有絕對運(yùn)行時(shí)地址的可執(zhí)行文件,就是可執(zhí)行的ELF格式的文件。
Linker 的連接動(dòng)作可以分為兩部分:
1) 符號解析。確定引用符號的指向。
2) 符號重定位。合并section, 分配運(yùn)行時(shí)環(huán)境地址,引用符號重定位。
符號解析:
在一個(gè)object文件中,有指令定義了符號,也有指令引用了符號??赡艽嬖谶@樣一種情況,一個(gè)被引用到的符號,有多重的定義。符號解析的作用就是確定,在這個(gè)object文件中,一個(gè)符號引用真正引用的是哪個(gè)符號。
在編譯的時(shí)候,除了在本文件中定義的全局變量會(huì)由編譯器生成一個(gè)符號表項(xiàng)之外,當(dāng)發(fā)現(xiàn)一個(gè)被引用到的符號在本文件中并沒有被定義,編譯器也會(huì)自動(dòng)產(chǎn)生一個(gè)符號表項(xiàng),把確定這些引用的工作留給linker。匯編器在匯編時(shí)將讀取這些符號表項(xiàng),生成.symtab。在讀取的過程中,如果發(fā)現(xiàn)有在無法確定的符號引用項(xiàng),匯編器會(huì)為這些符號額外生成一個(gè)數(shù)據(jù)項(xiàng),稱作重定位數(shù)據(jù)項(xiàng),存放于rel.text或rel.data section中,交由linker確定。下面是重定位數(shù)據(jù)項(xiàng)(relocation entry)的數(shù)據(jù)結(jié)構(gòu):
Typedef struct{
int offset;//指明需要被重定位的引用在object中的偏移,實(shí)際上就是需要被重定位的引用
//在object中的實(shí)際位置
int symbol;//這個(gè)被重定位的引用真實(shí)指向的符號
int type;//重定位類型:R_ARM_PC24:使用24bit的PC相對地址重定位引用
//R_ARM_ABS32:使用32bit絕對地址重定位引用
}Elf32_Rel
Linker 需要解析的,就是那些被生成了重定位數(shù)據(jù)項(xiàng)的引用。Linker將根據(jù)C語言定義的規(guī)則,對于每一個(gè)重定位數(shù)據(jù)項(xiàng),在輸入的各個(gè)object文件中查找適合的符號,把這個(gè)符號填入symbol項(xiàng)中。但是由于還不知道這個(gè)符號的真實(shí)地址,所以現(xiàn)在就算知道了引用的真實(shí)指向,但我們還是不能確定這個(gè)引用指向的地址。
符號重定位:
符號重定位用來解決上面的問題。Linker首先進(jìn)行section 的合并。Linker合并object文件的過程很簡單,一般就是相同屬性的section合并,例如不同object文件的.text section 將被合并成一個(gè).text。同樣,.symtab section也被合并成一個(gè).symtab。這里面涉及到兩個(gè)問題:
1) 各個(gè)object文件合并的順序。這個(gè)問題涉及到最終指令和符號的運(yùn)行地址。最為重要的是,究竟是哪個(gè)section排在最前頭?在ARM RAW 系統(tǒng)得開發(fā)過程中,這個(gè)最為重要。ARM系統(tǒng)CPU上電后,系統(tǒng)會(huì)自動(dòng)的從0x00000000地址取指令并執(zhí)行,這個(gè)地址上映射著存儲(chǔ)器。這個(gè)動(dòng)作是不可編程的。所以排在最前面的section一定要包含有程序的入口點(diǎn),否則系統(tǒng)無法正常運(yùn)行。
2) 輸入段與輸出端之間的對應(yīng)關(guān)系。理論上,任何section,都可以被隨意的映射到一個(gè)輸出段中。一個(gè).data section是可以與一個(gè).text section 組成輸出一個(gè).text的。當(dāng)然這樣的動(dòng)作毫無意義。我們必須告訴linker使用那些section作為輸入,產(chǎn)生一個(gè)輸出section.
以上這兩個(gè)問題,都是通過一個(gè)稱為連接腳本的文件控制的。Linker通過讀取連接腳本,來決定section 從輸入到輸出的映射,設(shè)置程序的入口點(diǎn),設(shè)置哪個(gè)section應(yīng)該在整個(gè)可執(zhí)行文件的頭部等問題。
連接腳本還有另外一個(gè)作用,那就是指定每個(gè)section的地址。在section 合并完成后,linker將跟據(jù).symtab,對符號進(jìn)行統(tǒng)一的編址,分配一個(gè)絕對的運(yùn)行時(shí)地址。這個(gè)地址是以section地址作為基地址的。假設(shè).text section的地址是0x00000000,那么.text里面的符號將以0x00000000這個(gè)地址作為基準(zhǔn)地址。指定section地址的工作也是由連接腳本完成。在嵌入式開發(fā)中常見的在編譯工程時(shí)需指定的text_base, data_base等參數(shù),最后會(huì)被加入到連接腳本中,從而完成section的地址分配。
以上兩步完成后,linker 執(zhí)行引用符號重定位操作。Linker遍歷.rel section (包括.rel text 和 .rel data),對于其中的每個(gè)數(shù)據(jù)項(xiàng),根據(jù)symbol域到.symtab 中查出相應(yīng)的引用的真實(shí)地址(經(jīng)過上面的地址分配,現(xiàn)在.symtab里面的符號都具有絕對的運(yùn)行地址),再根據(jù)offset域提供的偏移,將這個(gè)地址填入相應(yīng)的位置上。
至此,符號重定位工作全部完成。Linker刪除用于保存重定位信息的rel.text和rel.data section,加入一個(gè)segment header和 一個(gè).init section。生成可執(zhí)行的ELF格式的object文件。
Segment header保存了用于操作系統(tǒng)內(nèi)存映射的信息。.init section 包含了一個(gè)_init 的函數(shù)。程序加載時(shí),操作系統(tǒng)的程序加載器通過讀取segment header,將程序加載到用戶內(nèi)存空間,并根據(jù)segment header里面映射信息,分別將.text 段和.data段映射到適當(dāng)?shù)牡刂飞稀H缓笤僬{(diào)用.init中的_init函數(shù),完成初始化工作。
由于ELF文件具有通用性強(qiáng)的優(yōu)點(diǎn),現(xiàn)在流行的開發(fā)模式是,先通過編譯工具生成ELF文件格式的可執(zhí)行文件,在使用外部工具,抽離出ELF文件中的相應(yīng)部分,生成BIN文件。例如著名的GNU bootloader U-Boot,就采用了這種做法,編譯器工具集是GCC,BIN生成工具是elf2bin。ARM公司著名的開發(fā)環(huán)境ADS,雖然使用的是自家的armcc,和armcpp編譯器,但他們的工作方式卻是與GNU GCC如出一轍。