程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> ELF文件,elf

ELF文件,elf

編輯:關於C語言

ELF文件,elf


ELF文件格式是一個開發標准,各種UNIX系統的可執行文件都采用ELF格式,它有三種不同的類型:

  • 可重定位的目標文件
  • 可執行文件
  • 共享庫

現在分析一下上一篇文章中經過匯編之後生成的目標文件max.o和鏈接之後生成的可執行文件max的格式,從而理解匯編、鏈接和加載執行的過程。

一、目標文件

ELF文件格式提供了兩種不同的視角,在匯編器和鏈接器看來,ELF文件是由Section Header Table描述的一系列Section的集合,而執行一個ELF文件時,在加載器看來它是由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並不一定要位於文件開頭和結尾的,其位置由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,然後我們逐段分析。

ELF Header中描述了操作系統是UNIX,體系結構是80386.Section Header Table中有8個Section Header,在文件中的位置(或者叫文件地址)從180(0xc8)開始,每個40字節,共320字節,到文件地址0x1f3結束。這個目標文件沒有Program Header。

從Section Header中讀出各Section的描述信息,其中.text和.data是我們在匯編程序中聲明的Section,而其他Section是匯編器自動添加的。Addr是這些段加載到內存中的地址(程序中的地址都是虛擬地址),加載地址要在鏈接時填寫,現在空缺,所以是00000000.Off和Size兩列指出了各Section的文件地址,比如.data從文件地址0x60開始,一共0x24個字節,我們在max程序中定義了9個4字節的整數,一共是36個字節,十六進制則為0x24個。根據以上信息可以描繪出整個目標文件的布局:

(注意:這只是參考書上的截圖內存地址與上圖實際地址不同,僅供參考之用)

我們用hexdump工具把目標文件的字節全部打印出來看。

左邊一列是文件中的地址,中間是每個字節的16進制表示,右邊是把這些字節解釋成ASCII碼所對應的字符。中介有一個*號表示省略的部分全是0。.data段對應的是這一塊:

這一段將來要原封不動地加載到內存中。

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

可見.shstrtab中保存著各Section的名字,.strtab中保存著程序中用到的符號的名字。每個名字都是以'\0'結尾的字符串。

C語言的全局變量如果在代碼中沒有初始化,就會在程序加載時用0初始化。這種數據屬於.bss段,在加載時它和.data段一樣是可讀可寫的數據,但是在ELF文件中.data段需要占用一部分空間保存初始值,而.bss段則不需要。也就是說.bss段在文件中值占一個Section Header而沒有對應的Section,程序加載時.bss段占多大內存空間在Section Header中描述。

 

我們繼續分析readelf輸出的最後一部分,是從.rel .text和.symtab這兩個Section中讀出的信息

.rel .text告訴鏈接器指令中的哪些地方需要重定位。

.symtab是符號表。Ndx列是每個符號所在的Section編號,例如data_items在第三個Section裡(也就是.data)各Section的編號見Section Header Table。Value列是每個符號所代表的地址,在目標文件中。符號地址都是相對於該符號所在Section的相對地址,比如data_items位於.data段的開頭,所以地址是0。從Bind這一列可以看出_start這個符號是GLOBAL的,而其他符號是LOCAL的。

現在只剩下.text段沒有分析。objdump工具可以把程序中的機器指令反匯編,那麼反匯編的結果是否跟原來寫的匯編代碼一模一樣呢?對比:

左邊是機器指令的字節,右邊是反匯編結果。顯然所有的符號都被替換成地址了,比如je 23,注意沒有加$的數表示內存地址,而不表示立即數。這條指令後面的<loop_exit>並不是指令的一部分,而是反匯編器從.symtab和.strtab查到的符號名稱,寫在後面是為了有更好的可讀性。目前所有的跳轉指令和內存訪問指令(mov 0x0(,%edi,4), %eax)中的地址都是符號的相對地址,下一步鏈接器要修改這些指令,把其中的地址都改成加載時的內存地址,這些指令才能正確執行。

 

二、可執行文件

 

我們按上一節的步驟分析可執行文件max,看看鏈接器都做了什麼改動

在ELF Header中,Type改成了EXEC,由目標文件變成可執行文件了,Entry point改成了0x8048074(這是_start符號的地址),還可以看出多了兩個Program Header,少了兩個Section Header。

在Section Heade Table中 .text和.data的加載地址分別改成了0x8048074和0x80490a0。.bss段沒有用到,所以被刪掉了。.rel .text段就是用於鏈接過程的,鏈接完了就沒用了,所以也刪掉了。

多出來的Program Header Table描述了兩個Segment的信息。.text段和前面的ELF Header、Program Header Table一起組成一個Segment(FileSize指出總長度是0x9e), .data段組成另一個Segent(總長度是0x38)VirtAddr列指出第一個Segment加載到虛地址0x08048000,第二個Segment加載到地址0x080490a0。Fig列指出第一個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。

再看一下反匯編的結果:

指令中的相對地址都改成絕對地址了。我們仔細檢查一下都改了哪些地方。首先看跳轉指令,原來目標文件的指令是這樣:

......
11: 74 10 je 23 <loop_exit>
......
1d: 7e ef jle e <start_loop>
......
21: eb eb jmp e <start_loop>
......

現在改成了這樣:

......
8048085: 74 10 je 8048097 <loop_exit>
......
8048091: 7e ef jle 8048082 <start_loop>
......
8048095: eb eb jmp 8048082 <start_loop>
......

改了嗎?其實只是反匯編的結果不同了,指令根本沒改。為什麼不用改指令就能跳轉到新的地址呢?因為跳轉指令中指定的是相對於當前指令向前或向後跳多少字節,而不是指定一個完整的內存地址,內存地址有32位,這些跳轉指令只有16位,顯然也不可能指定一個完整的內存地址,這稱為相對跳轉。
再看內存訪問指令,原來目標文件的指令是這樣:

......
5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax
......
14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax
......

現在改成了這樣:

......
8048079: 8b 04 bd a0 90 04 08 mov
0x80490a0(,%edi,4),%eax
......
8048088: 8b 04 bd a0 90 04 08 mov
0x80490a0(,%edi,4),%eax
......

指令中的地址原本是0x0000 0000,現在改成了0x0804 09a0(注意是小端字節序)。那麼鏈接器怎麼知道要改這兩處呢?是根據原來目標文件中的 .rel.text 段提供的重定位信息來改的:

......
Relocation section '.rel.text' at offset 0x2b0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000008 00000201 R_386_32 00000000 .data
00000017 00000201 R_386_32 00000000 .data
......

第一列 Offset 的值就是 .text 段需要改的地方,在 .text 段中的相對地址是8和0x17,正是這兩條指令中00 00 00 00的位置。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved