原文地址:https://blog.csdn.net/daide2012/article/details/73065204
一、 引言
在講解ELF文件格式之前,我們來回顧一下,一個用C語言編寫的高級語言程序是從編寫到打包、再到編譯執行的基本過程,我們知道在CPU上執行的是低級別的機器語言,從高級語言到低級別的機器語言肯定是要經過翻譯過程,這個過程大體的過程如下圖所示:
在Unix系統中,從源文件到可執行目標文件是由編譯驅動程序完成的,如大名鼎鼎的gcc,翻譯過程包括圖中的是個階段;
Ø 預處理階段
預處理器(cpp)根據以字符#開頭的命令修給原始的C程序,結果得到另一個C程序,通常以.i作為文件擴展名。主要是進行文本替換、宏展開、刪除注釋這類簡單工作。
對應的命令:linux> gcc -E hello.c hello.i
Ø 編譯階段
編譯器將文本文件hello.i翻譯成hello.s,包含相應的匯編語言程序
對應的命令:linux> gcc -S hello.c hello.s
Ø 匯編階段
將.s文件翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程序的格式,並將結果保存在目標文件.o中(把匯編語言翻譯成機器語言的過程)。
把一個源程序翻譯成目標程序的工作過程分為五個階段:詞法分析;語法分析;語義檢查和中間代碼生成;代碼優化;目標代碼生成。主要是進行詞法分析和語法分析,又稱為源程序分析,分析過程中發現有語法錯誤,給出提示信息。
對應的命令:linux> gcc -c hello.c hello.o
Ø 鏈接階段
此時hello程序調用了printf函數。 printf函數存在於一個名為printf.o的單獨的預編譯目標文件中。 鏈接器(ld)就負責處理把這個文件並入到hello.o程序中,結果得到hello文件,一個可執行文件。最后可執行文件加載到儲存器后由系統負責執行, 函數庫一般分為靜態庫和動態庫兩種。靜態庫是指編譯鏈接時,把庫文件的代碼全部加入到可執行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了。其后綴名一般為.a。動態庫與之相反,在編譯鏈接時並沒有把庫文件的代碼加入到可執行文件中,而是在程序執行時由運行時鏈接文件加載庫,這樣可以節省系統的開銷。動態庫一般后綴名為.so,gcc在編譯時默認使用動態庫。
二、目標文件
由上面的過程,我們可以看出在經過匯編器和連接器作用后都會輸出一個目標文件,那這兩個目標文件有什么樣的區別呢?說到這里我們先引入目標文件的形式
2.1 三種目標文件形式
(1)可重定位目標文件:包含二進制代碼和數據,其形式可以和其他目標文件進行合並,創建一個可執行目標文件
(2)可執行目標文件:包含二進制代碼和數據,可直接被加載器加載執行
(3)共享目標文件:可被動態的加載和鏈接(本文暫時不討論)
由此我們可知由匯編器生成的就是可重定位目標文件,經過鏈接器作用后才生成可執行目標文件,鏈接器的作用就是以一組可重定位目標文件作為輸入,生成可加載和運行的可執行目標文件,具體需要完成以下兩個工作:
Ø 符號解析:符號解析的目的是將目標文件中每個符號(靜態變量、函數、全局變量)和其定義進行關聯
Ø 重定位:將每個符號的定義與具體在虛擬內存中的位置進行關聯
最終生成可執行目標文件
說到這里好像還是沒有說清楚這兩種目標文件有什么區別,我們還是先把這個問題放一下,相信你看完下一節,應該會有答案,下面我們開始引入目標文件ELF文件。
三、ELF文件
目標文件再不同的系統或平台上具有不同的命名格式,在Unix和X86-64 Linux上稱為ELF(Executable and Linkable Format, ELF)。
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個。根據以上信息可以描繪出整個目標文件的布局。
起始文件地址 |
Section或Header |
0 |
ELF Header |
0x34 |
.text |
0x60 |
.data |
0x98 |
.bss(此段為空) |
0x98 |
.shstrtab |
0xc8 |
Section Header Table |
0x208 |
.symtab |
0x288 |
.strtab |
0x2b0 |
.rel.text |
這個文件不大,我們直接用hexdump或者使用010 Editor
工具把目標文件的字節全部打印出來看。
3.1 .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
段,以后我們會看到這樣的例子。
3.2.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
指示聲明過的符號。
3.3 .text節

在看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。
再看一下反匯編的結果:
到此為止ELF文件的問題已介基本介紹,關於共享目標文件的格式和加載過程將在后續補上。
一、 引言
在講解ELF文件格式之前,我們來回顧一下,一個用C語言編寫的高級語言程序是從編寫到打包、再到編譯執行的基本過程,我們知道在CPU上執行的是低級別的機器語言,從高級語言到低級別的機器語言肯定是要經過翻譯過程,這個過程大體的過程如下圖所示:
在Unix系統中,從源文件到可執行目標文件是由編譯驅動程序完成的,如大名鼎鼎的gcc,翻譯過程包括圖中的是個階段;
Ø 預處理階段
預處理器(cpp)根據以字符#開頭的命令修給原始的C程序,結果得到另一個C程序,通常以.i作為文件擴展名。主要是進行文本替換、宏展開、刪除注釋這類簡單工作。
對應的命令:linux> gcc -E hello.c hello.i
Ø 編譯階段
編譯器將文本文件hello.i翻譯成hello.s,包含相應的匯編語言程序
對應的命令:linux> gcc -S hello.c hello.s
Ø 匯編階段
將.s文件翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程序的格式,並將結果保存在目標文件.o中(把匯編語言翻譯成機器語言的過程)。
把一個源程序翻譯成目標程序的工作過程分為五個階段:詞法分析;語法分析;語義檢查和中間代碼生成;代碼優化;目標代碼生成。主要是進行詞法分析和語法分析,又稱為源程序分析,分析過程中發現有語法錯誤,給出提示信息。
對應的命令:linux> gcc -c hello.c hello.o
Ø 鏈接階段
此時hello程序調用了printf函數。 printf函數存在於一個名為printf.o的單獨的預編譯目標文件中。 鏈接器(ld)就負責處理把這個文件並入到hello.o程序中,結果得到hello文件,一個可執行文件。最后可執行文件加載到儲存器后由系統負責執行, 函數庫一般分為靜態庫和動態庫兩種。靜態庫是指編譯鏈接時,把庫文件的代碼全部加入到可執行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了。其后綴名一般為.a。動態庫與之相反,在編譯鏈接時並沒有把庫文件的代碼加入到可執行文件中,而是在程序執行時由運行時鏈接文件加載庫,這樣可以節省系統的開銷。動態庫一般后綴名為.so,gcc在編譯時默認使用動態庫。
二、目標文件
由上面的過程,我們可以看出在經過匯編器和連接器作用后都會輸出一個目標文件,那這兩個目標文件有什么樣的區別呢?說到這里我們先引入目標文件的形式
2.1 三種目標文件形式
(1)可重定位目標文件:包含二進制代碼和數據,其形式可以和其他目標文件進行合並,創建一個可執行目標文件
(2)可執行目標文件:包含二進制代碼和數據,可直接被加載器加載執行
(3)共享目標文件:可被動態的加載和鏈接(本文暫時不討論)
由此我們可知由匯編器生成的就是可重定位目標文件,經過鏈接器作用后才生成可執行目標文件,鏈接器的作用就是以一組可重定位目標文件作為輸入,生成可加載和運行的可執行目標文件,具體需要完成以下兩個工作:
Ø 符號解析:符號解析的目的是將目標文件中每個符號(靜態變量、函數、全局變量)和其定義進行關聯
Ø 重定位:將每個符號的定義與具體在虛擬內存中的位置進行關聯
最終生成可執行目標文件
說到這里好像還是沒有說清楚這兩種目標文件有什么區別,我們還是先把這個問題放一下,相信你看完下一節,應該會有答案,下面我們開始引入目標文件ELF文件。
三、ELF文件
目標文件再不同的系統或平台上具有不同的命名格式,在Unix和X86-64 Linux上稱為ELF(Executable and Linkable Format, ELF)。
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個。根據以上信息可以描繪出整個目標文件的布局。
起始文件地址 |
Section或Header |
0 |
ELF Header |
0x34 |
.text |
0x60 |
.data |
0x98 |
.bss(此段為空) |
0x98 |
.shstrtab |
0xc8 |
Section Header Table |
0x208 |
.symtab |
0x288 |
.strtab |
0x2b0 |
.rel.text |
這個文件不大,我們直接用hexdump或者使用010 Editor
工具把目標文件的字節全部打印出來看。
3.1 .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
段,以后我們會看到這樣的例子。
3.2.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
指示聲明過的符號。
3.3 .text節

在看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。
再看一下反匯編的結果:
到此為止ELF文件的問題已介基本介紹,關於共享目標文件的格式和加載過程將在后續補上。