源文件到可執行文件流程
編譯:.c 文件變成 .s 匯編文件
匯編:.s 文件變成 .o 可重定位的目標文件
鏈接:一個或多個.o 文件變成一個可執行文件
ELF 文件和 BIN 文件的區別
BIN文件是直接的二進制文件,內部沒有地址標記。bin文件內部數據按照代碼段或者數據段的物理空間地址來排列。一般用編程器燒寫時從00開始,而如果下載運行,則下載到編譯時的地址即可。
在 Linux OS上,為了運行可執行文件,他們是遵循ELF格式的,通常gcc -o test test.c,生成的test文件就是ELF格式的,這樣就可以運行了,執行elf文件,則內核會使用加載器來解析elf文件並執行。
在Embedded中,如果上電開始運行,沒有OS系統,如果將ELF格式的文件燒寫進去,包含一些ELF文件的符號表字符表之類的section,運行碰到這些,就會導致失敗,如果用objcopy生成純粹的二進制文件,去除掉符號表之類的section,只將代碼段數據段保留下來,程序就可以一步一步運行。
elf文件里面包含了符號表等。BIN文件是將elf文件中的代碼段,數據段,還有一些自定義的段抽取出來做成的一個內存的鏡像。並且elf文件中代碼段數據段的位置並不是它實際的物理位置。他實際物理位置是在表中標記出來的。
ELF 文件格式詳解
ELF文件格式是一個開放標准,各種UNIX系統的可執行文件都采用ELF格式,它有三種不同的類型:
可重定位的目標文件(Relocatable,或者Object File)
可執行文件(Executable)
共享庫(Shared Object,或者Shared Library)
符號表機制(readelf -s XXX)
符號表保存了程序實現或使用的所有全局變量和函數,如果程序引用一個自身代碼未定義的符號,則稱之為未定義符號,這類引用必須在靜態鏈接期間用其他目標模塊或者庫解決,或在加載時通過動態鏈接解決
ELF文件格式提供了兩種不同的視角,在匯編器和鏈接器看來,ELF文件是由Section Header Table描述的一系列Section的集合,而執行一個ELF文件時,在加載器(Loader)看來它是由Program Header Table描述的一系列Segment的集合。

左邊是從匯編器和鏈接器的視角來看這個文件,開頭的ELF Header描述了體系結構和操作系統等基本信息,並指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在匯編和鏈接過程中沒有用到,所以是可有可無的,Section Header Table中保存了所有Section的描述信息。右邊是從加載器的視角來看這個文件,開頭是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加載過程中沒有用到,所以是可有可無的。注意Section Header Table和Program Header Table並不是一定要位於文件開頭和結尾的,其位置由ELF Header指出,上圖這么畫只是為了清晰。
我們在匯編程序中用.section聲明的Section會成為目標文件中的Section,此外匯編器還會自動添加一些Section(比如符號表)。Segment是指在程序運行時加載到內存的具有相同屬性的區域,由一個或多個Section組成,比如有兩個Section都要求加載到內存后可讀可寫,就屬於同一個Segment。有些Section只對匯編器和鏈接器有意義,在運行時用不到,也不需要加載到內存,那么就不屬於任何Segment。
目標文件需要鏈接器做進一步處理,所以一定有Section Header Table;可執行文件需要加載運行,所以一定有Program Header Table;而共享庫既要加載運行,又要在加載時做動態鏈接,所以既有Section Header Table又有Program Header Table。
下面用readelf工具讀出可重定位的目標文件max.o的ELF Header和Section Header Table,然后我們逐段分析。

接下來我們來看Section Header Table格式

從Section Header中讀出各Section的描述信息,其中.text和.data是我們在匯編程序中聲明的Section,而其它Section是匯編器自動添加的。Addr是這些段加載到內存中的地址(我們講過程序中的地址都是虛擬地址),加載地址要在鏈接時填寫,現在空缺,所以是全0。Off和Size兩列指出了各Section的文件地址,比如.data從文件地址0x60開始,一共0x38個字節,回去翻一下程序,.data中定義了14個4字節的整數,一共是56個字節,也就是0x38個。根據以上信息可以描繪出整個目標文件的布局。
這個文件不大,我們直接用hexdump或者使用010 Editor工具把目標文件的字節全部打印出來看。

.shstrtab和.strtab
.shstrtab和.strtab這兩個Section中存放的都是ASCII碼:

見.shstrtab中保存着各Section的名字,.strtab中保存着程序中用到的符號的名字。每個名字都是以'\0'結尾的字符串。
我們知道,C語言的全局變量如果在代碼中沒有初始化,就會在程序加載時用0初始化。這種數據屬於.bss段,在加載時它和.data段一樣都是可讀可寫的數據,但是在ELF文件中.data段需要占用一部分空間保存初始值,而.bss段則不需要。也就是說,.bss段在文件中只占一個Section Header而沒有對應的Section,程序加載時.bss段占多大內存空間在Section Header中描述。在我們這個例子中沒有用到.bss段,以后我們會看到這樣的例子。
.rel.text和.symtab
我們繼續分析readelf輸出的最后一部分,是從.rel.text和.symtab這兩個Section中讀出的信息。

.rel.text告訴鏈接器指令中的哪些地方需要重定位,我們在下一節討論。
.symtab是符號表。Ndx列是每個符號所在的Section編號,例如data_items在第3個Section里(也就是.data),各Section的編號見Section Header Table。Value列是每個符號所代表的地址,在目標文件中,符號地址都是相對於該符號所在Section的相對地址,比如data_items位於.data段的開頭,所以地址是0,_start位於.text段的開頭,所以地址也是0,但是start_loop和loop_exit相對於.text段的地址就不是0了。從Bind這一列可以看出_start這個符號是GLOBAL的,而其它符號是LOCAL的,GLOBAL符號是在匯編程序中用.globl指示聲明過的符號。
.text節
通過使用objdump工具可以把程序中的機器指令進行反匯編(Disassemble),得到其匯編代碼

可執行文件
先看可執行文件header的變化

在看section header的變化

.text和.data的加載地址分別改成了0x08048074和0x0804 90a0。.bss段沒有用到,所以被刪掉了。.rel.text段就是用於鏈接過程的,鏈接完了就沒用了,所以也刪掉了。
在看多出來的兩個program header

多出來的Program Header Table描述了兩個Segment的信息。.text段和前面的ELFHeader、Program Header Table一起組成一個Segment(FileSiz指出總長度是0x9e),.data段組成另一個Segment(總長度是0x38)。VirtAddr列指出第一個Segment加載到虛擬地址0x0804 8000(注意在x86平台上后面的PhysAddr列是沒有意義的),第二個Segment加載到地址0x0804 90a0。Flg列指出第一個Segment的訪問權限是可讀可執行,第二個Segment的訪問權限是可讀可寫。最后一列Align的值0x1000(4K)是x86平台的內存頁面大小。在加載時要求文件中的一頁對應內存中的一頁,對應關系如下圖所示。

這個可執行文件很小,總共也不超過一頁大小,但是兩個Segment必須加載到內存中兩個不同的頁面,因為MMU的權限保護機制是以頁為單位的,一個頁面只能設置一種權限。此外還規定每個Segment在文件頁面內偏移多少加載到內存頁面仍然偏移多少,比如第二個Segment在文件中的偏移是0xa0,在內存頁面0x0804 9000中的偏移仍然是0xa0,所以是從0x0804 90a0開始,這樣規定是為了簡化鏈接器和加載器的實現。從上圖也可以看出.text段的加載地址應該是0x0804 8074,也正是_start符號的地址和程序的入口地址。
原來目標文件符號表中的Value都是相對地址,現在都改成絕對地址了。此外還多了三個符號__bss_start、_edata和_end,這些是在鏈接過程中添進去的,加載器可以利用這些信息把.bss段初始化為0。
再看一下反匯編的結果:

