最近的lab裡面有ELF文件相關的,所以成這個幾乎,學點ELF的東西。
ELF,是一種文件格式。暫時,只看可執行文件的ELF文件格式。
首先,給出文件的格式的布局圖:
光看這個很難理解,所以寫一個小的程序,用readelf來結合的看。
程序比較簡單:
#include <stdio.h> #include <stdlib.h> int data[100] ={0}; int bss[100]; int main() { int i=0; for(i=0; i<100; i++) bss[i] = i; printf("the bss[3]= %d\n", bss[3]); return 1; }
首先,通過readelf -h 命令,來看elf頭:
首先,第一個magic,魔數,這個主要是程序用來確認讀入的是否是elf文件頭,其中,第一個7f是默認的,後面的45,4c,46就是ascii碼裡面的elf相對於的碼值,後面的01,沒有實際意義。每次程序在讀取elf頭文件的時候,都會確認魔數是否正確,以防讀入的不是elf文件。
接下來的class,Data,Version,OS/ABI, ABI Version type, machine version 都是一些關於機器,系統還有文件版本的一些信息,不是這次的主要內容,看看就好。
接下來的Entry point address 0x8048330 表示程序的入口地址,即程序載入完成後,第一條指令從這個地方開始,從指令上來說,就是在整個程序建立了進程,將相應的虛擬地址映射載入內存後,做完了所有的准備工作之後,將要開始執行程序了,此時,將eip 置為0x8048330這個值。剛開始學C的時候,很多人都認為,之所以要有main函數,是因為他是程序的入口,程序執行的第一條指令就是main函數。如果是這樣,那在0x8048330這個位置的函數就應該是main函數了。
通過程序來看,用objdump將程序反匯編:
可以看到,程序的08048330的部分是一個叫-start的一個函數,並不是我們想當然的main。為什麼?來看看<_start>函數的主要內容,掃一眼,就發現,這個函數的主要內容是在要存相關的寄存器,後面跳轉到一個叫<_libc_start_main@plt>函數下面去了,也就是說,在程序真正的執行main操作之前,還進行了其他的函數操作,也就是main並不是真正的第一個執行的函數。那在main之前,到底那些函數都干了什麼呢?
其實很簡單。main函數在開始的時候,裡面的變量,直接開始的時候就在棧裡,然後一開始就可以直接使用malloc和new等來申請堆空間,那棧和堆的剛開始的設置地址是什麼?在main裡好像沒有設置吧?還有比如stdin,stdout等都沒有打開,所以,main前面的_start等函數做的就是這種工作,初始化堆棧信息,並且打開標准輸入輸出等文件。
簡單來說,就是一下內容:
//***********************************************
__start:
init stack;
init heap;
open stdin;
open stdout;
open stderr;
:
push argv;
push argc;
call _main; (調用 main)
:
destory heap;
close stdin;
close stdout;
close stderr;
:
call __exit;
**********************************************/
所以,main只是整個程序的中間函數,並不是程序最開始執行的函數!!!
接下來,Start of program headers: 52 (bytes into file)
Start of section headers: 5120 (bytes into file)
這就是程序表頭和Section 表頭的地址。這個地址,表示程序頭和Section 表頭的首地址距離ELF文件頭地址的偏移量。
舉個例子,程序總是從磁盤讀入內存的。假設程序在磁盤的位置是0x1000,那程序頭和Section 表頭在磁盤的位置就是0x1052和0x6210,,注意,這個是磁盤的地址。
什麼是程序表頭和Section 表頭。通俗的來講,程序表是一張表,裡面有程序需要從磁盤載入內存的所有內容的相關信息。而系統把這些需要載入內存的內容分成了一個一個的塊,這些塊需要一張表來管理並且記錄他們的信息。而這個表就是程序表。相應的,Section 表就是記錄每個Section 的相應信息的一張表。程序表和Section 表,在程序中的表示方法,就是兩個結構體的數組,所以程序表頭和Section 表頭就是兩個結構體數組的首地址。
下面的flag,應該是標志位什麼的,暫時沒搞明白。
在下面: Size of this header: 52 (bytes)
這個就表示,這張elf頭文件大小事52字節,參考前面的Start of program headers: 52 (bytes into file),程序頭的內容在磁盤中也是從elf首字節下面的低52個字節裡面,這就表示在磁盤中,elf頭表之後,馬上就是程序頭的內容,和上面的elf的布局圖相符合。
Size of program headers: 32 (bytes)
這個表示每個程序頭表中,每一項的大小。前面說過,程序頭表就是一個結構體的數組,那這個32byte就代表這個結構體的大小。
Number of program headers: 9
這個代表程序頭表中,程序頭的個數。和上面的程序頭表的每一項大小相乘,就是整個程序頭表的大小。
Size of section headers: 40 (bytes)
Number of section headers: 36
這兩個數據,就是Section 表的數據,和程序頭表的數據時一樣的。
在磁盤中,這張elf頭文件的形式是一個struct, 整個的內容和下面的代碼相似:
struct Elf { uint32_t e_magic; // must equal ELF_MAGIC uint16_t e_type; uint16_t e_machine; uint32_t e_version; uint32_t e_entry; uint32_t e_phoff; uint32_t e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; };
這個和上面的readelf得到的結果是一一對應的。
接下來,看program header
這個就是程序頭表裡面的內容,這些程序頭記錄著程序需要拷貝到內存上的所有內容,需要把這些內容拷貝到內存上,才能實現程序的運行。這個程序的程序頭總共有9項。每一項都有相應的屬性,一項一項來看。
可以看到,第一項的type是PHDR,他表示我們要保存這項內容是程序頭表。其中,offset表示這項內容保存的起始地址與程序頭地址的偏移。即系統要通過這個偏移地址,從磁盤中讀取相應的程序頭的內容到內存中。有了從磁盤讀入的地方,那肯定要有放到內存中的地方。VirtAddr就表示這部分內容需要要放到虛擬內存中的起始地址,後面的PhysAddr就是為了兼容采用實地址模式的系統。後面的FileSize和memSize分別表示程序頭在文件中和在內存中的大小。(兩個大小可以不一樣,但一定是filesize<=memsize).後面的flg就是表示這個程序頭的標志位,其中R表示可讀,W表示可寫,E表示可執行。最後的Align就表示這段程序頭的對齊方式。其中0x4就表示4字節對齊,0x1000就表示4K對齊。
下面來驗證一下:第一段程序頭的type指出了這段程序頭裡的內容是表示程序頭表的。他的起始文件位置是偏離程序起始文件位置0x34個字節的地方。通過計算可以知道,0x34=52,和前面的elf頭文件中程序頭表的起始位置吻合。後面的filesize是0x120=288byte, 通過elf知道,程序頭表裡總共有9項,每一項程序頭占了32個字節,這樣整個程序頭的大小就是32*9=288.這個也吻合。接下來通過gdb查看程序在0x8048034的內容。
由於每一項程序頭是的大小事32byte=0x20byte,所以上面兩行代表一個程序頭,可以看到和readelf給出的內容是完全吻合的,也就是在這塊地址空間中,存的是相應的程序頭表。
接下去的8項程序頭,都是一樣的,只是type不一樣,每種type所代表的含義如下:
PHDR保存程序頭表。
INTERP指定在程序已經從可執行映射到內存之後,必須調用解釋器。在這裡解釋器並不意味著二進制文件的內存必須由另一個程序解釋。它指的是這樣的一個程序:通過鏈接其他庫,來滿足未解決的引用。
LOAD表示一個從二進制文件映射到虛擬地址空間的段。其中保存了常量數據(如字符串),程序的目標代碼等等。
DYNAMIC段保存了其他動態鏈接器(即,INTERP中指定的解釋器)使用的信息。
NOTE保存了專有信息。
仔細觀察會發現,在兩個屬性為load的程序頭中,包含了其他7個程序頭中的所有段內容。這點,從載入的虛擬內存上的地址范圍也可以發現這個問題。所以,在程序載入時,應該只需要載入type為load的兩個程序頭就可以了,而其他的程序頭只是為了方便查找相應的內容的。
在上面的這章程序表的下面,還有每項程序頭所包含的段的信息:
可以再裡面看到比較熟悉的.text, .data 和.bss段。這就表示,所有程序頭其實是程序裡面section的一部分。整個程序按照一定的方法,被劃分了若干個section,而程序頭就是在所有section中,需要被載入到內存的那部分section。從真個程序文件的架構也可以看到,在那裡面根本就沒有程序頭的部分。
接下來:section header
由於可執行文件的段比較多,所以就不全部截出來了。只給出部分的段:
這裡主要看text,data,bss三個段。
其中,text表示的是程序的代碼段。可以看到,他在內存中的地址是0x8048330,就是整個程序的入口地址。
後面的data和bss段。首先看.bss段,可以看到,bss段下面的comment段在文件中的偏移地址是一樣的。這也就是大家常說的,.bss段在文件中是不占大小的。這是因為,bss段代表的是未初始化的全局變量。在C裡面,未初始化的全局變量會被初始化為0,因此就不用在文件中給bss分配空間,因為只要變量屬於bss段,那他就是0.而bss段中的變量,在內存中的總大小,就可以通過section段表來記錄。也就是上面的bss段的size部分。
可以通過上面的程序頭部分進行驗證。通過上面的程序頭中,.data和bss段都是出於程序頭的03項,
單獨把第三項拉出來:
可以看到,在這一項的程序頭中,filesize金額memsize是不一樣的,兩者相間,0x45c-0x100=0x35c。比.bss段的大小多了c字節大小,這個多出來的c字節的大小不知道怎麼回事。我猜想應該和字節對齊有關系。因為這段程序頭在內存中的起始位置是0x0849f14, 加上內存中的大小,就是0x084a370。如果沒有這個多出來的c字節,那可能下面的內容要實現字節對齊就比較麻煩,所以編譯器認為的加了一個c字節上去。
那bss段的變量是怎麼被初始化為0的?
在程序被載入內存的時候,只需要從文件拷貝filesize大小的內容進入內存,然後剩下的部分,將其全部以0填充就可以了。
從上面的程序頭中包含的段映射可以看到,bss段是被放在他所在的程序頭的最下面的,甚至是整個程序在內存空間中最下面的位置。所以通過上面的方法,就可以將bss段所屬的變量全部清0,就完成了初始化為0的操作。而變量可以通過符號表來解決,其所在的位置。
符號表(只列出data和bss的信息):
在程序中,兩個全局變量:
<span>在符號表裡面,value應該就是變量的首地址,後面的size就是變量所占的內存大小。由於兩個變量都是int型的大小為100的數組,所以大小為400。後面的NDX指出了符號在哪個段,從段表中找段25,可以看到,段25就是bss段。可能是由於我data[0]的值也是0,這樣就導致了data和bss兩個值都是0,所以編譯器優化,就把兩個變量都放到了bss段。重新編一個簡單的程序:
#include <stdio.h> #include <stdlib.h> int d=10; int b; int main() { int i=0; printf("the out is %d\n", b+d+i); return 1; }
符號表和段表為:
可以看到,b,d所對應的的段序號正是data和bss段,而d的地址為0x0804a014, data的起始地址為0x0804a00c,size為c,所以d就是存儲在data段的後4個字中的大小。而b變量也正好存儲在bss段的後4個字節中。
從後來改過的程序可以看到,程序是通過符號表來進行初始化全局變量的的過程和上面的分析是符合的。
總結:
整個程序,在文件中,是通過elf頭文件來管理的,elf中記錄了程序中所有內容的信息。整個程序,首先分成了n個段,這些段的信息都存儲在程序的段表裡面。而程序頭就是在所有的段中,需要載入內存的段。程序頭由程序頭表來記錄其信息,包括其在文件中的位置和載入的內存的位置,還有其在文件中的大小和在內存中的大小等關鍵信息。所以在程序運行的時候,首先就是要找到程序頭表的位置,把程序頭表中所表示的段,根據程序頭中的信息,載入到內存中,然後再運行程序。
通過這個elf的學習,搞明白了幾個以前一直比較模糊的問題:
bss段為什麼在文件中是0字節的
在程序中的全局變量是如何初始化的。
還有以前一直沒弄明白bss,data段和內存管理中的分段的那個段到底是不是一個東西,現在對這些都比較明白了,學學這個還是有好處的,可以讓一些問題更加清晰。
版權聲明:本文為博主原創文章,未經博主允許不得轉載。