C語言 目標文件和可執行文件(ELF文件)

1.C語言創建程序

1.1C語言創建(分爲4個步驟)

  • 編輯
  • 編譯
  • 鏈接
  • 執行

編輯:就是創建和修改C程序的源代碼-我們編寫的程序稱爲源代碼。
編譯:就是將源代碼轉換爲機器語言。編譯器的輸出結果成爲目標代碼,存放它們的文件稱爲目標文件。擴展名爲.o或者.obj。
(該部分編譯是指彙編器編譯彙編語言或者編譯器編譯高級語言)
鏈接:鏈接器將源代碼由編譯器產生的各種模塊組合起來,再從C語言提供的程序庫中添加必要的代碼模塊,將它們組成一個可執行的文件。在windows下擴展名爲.exe,Unix下無擴展名。
執行:運行程序。

1.2什麼是源代碼,目標文件,可執行文件。

源代碼 ——源文件就是存放程序代碼的文件。通常我們編輯代碼的文件就是源文件。

  • 源代碼相對目標代碼和可執行代碼而言的。
  • 源代碼就是用匯編語言和高級語言寫出來的地代碼。

目標文件——指源代碼經過編譯程序產生的能被cpu直接識別二進制代碼。

  • 目標代碼指計算機科學中編譯器或彙編器處理源代碼後所生成的代碼,它一般由機器代碼或接近於機器語言的代碼組成。
  • 目標文件包括着機器代碼(可直接被計算機中央處理器履行)和代碼在運行時使用的數據,如重定位信息,如用於鏈接或調試的程序符號(變量和函數的名字),另外還包括其他調試信息。
gcc -c main.c 
編譯main.c ,生成目標文件main.o,但不進行link. 
gcc -o main.o
鏈接成可執行文件main。

可執行文件——可執行代碼就是將目標代碼連接後形成的可執行文件,當然也是二進制的。 連接程序系統庫文件連接就生成可執行文件。

例如:*.obj是程序編譯之後生成的目標文件,連接程序再將這個文件與系統庫文件連接就生成可執行文件

 

根據上面的圖,我們可以看到鏈接器還額外鏈接了2個部分。

目標代碼文件中所缺少的第一個元素是一種叫做啓動代碼(Start-up code)的東西,此代碼相當於您的程序和操作系統之間的接口。例如你可以在dos 或Linux下運行一個 IBM PC 兼容機,在兩種情況中硬件是相同的,所以都會使用同樣的目標代碼,但是 DOS與Linux要使用不用的啓動代碼,因爲這兩種系統處理程序的方式不同的。

所缺少的第二個元素是庫例程的代碼。幾乎所有C程序都利用標準庫中所包含的例程(稱爲函數)。例如,程序中的函數printf()。目標代碼文件不包含這一函數的指令。實際代碼存儲在另一個稱爲“庫”的文件中,庫文件中包含許多函數的目標代碼。

鏈接器的作用是將這3個元素(目標代碼、系統的標準啓動代碼和庫代碼)結合在一起,並將他們存放在單個文件,即可執行文件中。對庫代碼來說,鏈接器只從庫中提取您所使用的函數所需的代碼。

可以得出結論:目標文件和可執行文件都是由機器語言指令組成的。但目標文件只包含您所編寫的代碼轉換成的機器語言,而可執行文件還包含您所使用的庫例程以及啓動代碼。

下面這幅圖能大致說明一下鏈接的情況。

這是一個main.o目標代碼,內部有main,foo,bar三個函數。

U main表示main這個符號在crtl1.o中用到了,但是沒有定義。因此需要main.o提供定義並和crtl1.o鏈接在一起。main整個程序的入口實際上是_crtl1.o中的 _start,它做了一些初始化工作(啓動歷程),然後調用C代碼中提供的main.c函數。libc是運行時候動態鏈接libc共享庫(庫中包含常用的函數)。

所以程序的入口點其實是_start,main函數實際上是被_start調用。

1.4gcc命令圖


2.ELF文件(該部分分析目標文件和可執行文件的,涉及部分彙編指令)

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

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

ELF文件格式提供了兩種不同的視角,在彙編器和鏈接器看來,ELF文件是由Section HeaderTable描述的一系列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和ProgramHeader Table並不是一定要位於文件開頭和結尾的,其位置由ELF Header指出,上圖這麼畫只是爲了清晰。

目標文件需要鏈接器做進一步處理,所以一定有Section Header Table;可執行文件需要加載運行,所以一定有Program Header Table;而共享庫既要加載運行,又要在加載時做動態鏈接, 所以既有Section Header Table又有Program Header Table。

  • section:C語言內存中的.text,.data,.bss.....
  • Segment:是指在程序運行時加載到內存的具有相同屬性的區域,由一個或多個Section組成,比如有兩個Section都要求加載到內存後可讀可寫,就屬於同一個Segment。有些Section只對彙編器和鏈接器有意義,在運行時用不到,也不需要加載到內 存,那麼就不屬於任何Segment 。

2.1重定位目標文件

在進行該部分之前,我們先查看一下網上的部分重定位目標文件的資料。

資料一:

彙編器所產生的目標文件至少包括三個區,即文本區(text),數據區(data)和bss區。文本區一般包括程序的代碼和常量,數據區通常存放全局變量等內容,bss區用於存放未初始化的變量或作爲公共變量存儲空間。在一個目標文件中,其text區從地址0開始,隨後是data區,再後面是bss區。而要運行程序,必須裝載到內存中,所以這些區的地址需要在內存中重新安排,也就是重定位。

資料二:

編譯器編譯後產生的目標文件是可重定位的程序模塊,並不能直接運行,鏈接就是把目標文件和其他分別進行編譯生成的程序模塊(如果有的話)及系統提供的標準庫函數連接在一起,生成可運行的可執行文件的過程。
重定位是鏈接器在完成符號解析後(知道了各個輸入模塊的代碼段和數據段的大小)的一個步驟,其作用顧名思義就是重新定位,確定比如指令,全局變量等在運行時的存儲器地址。

資料三:

比如說兩個編譯後的可重定位目標文件obj1.o和obj2.o
在obj1.o裏面定義了一個全局變量glob(在obj1裏面記錄了glob相對於該文件數據段的相對地址), 而obj2.0裏面又引用了這個全局變量glob。
鏈接的重定位就是要確定在鏈接後的可執行程序中glob的地址,而不是相對於obj1的地址,從而使obj2也能通過地址調用glob。
當然重定位並不只是全局變量,還包括外部函數,指令等運行時地址的確定

資料四:

當你在程序中寫上一個全局變量或者是一個函數時,這個定位過程會經歷幾個階段:
1.在這個目標文件中的相對定位,一個目標文件中對此文件中的所有函數,變量進行符號描述,比如一個變量A,它所佔的相對地址是多少?是全局的?或者是靜態的,或者是外部的??
2.在連接多個目標成一個可執行文件時,會再次對這個變量進行重定位,也就是在這個可執行文件中進行對此變量進行描述,同目標文件中的描述差不多,只不過此變量不再有外部,內部之分,都成了本地變量,並且會將所有全局變量存放在一定的邏輯地址中,這是通過連接腳本文件與各個目標文件中的相對地址共同決定的
3.最終的操作系統加載這個可執行文件時,會對這些變量與函數地址再次進行重定位,其方式就是首先分析這個可執行文件中的不同段,讀出相應的描述表,然後通過邏輯地址與物理地址進行映射出,最終就將可執行的二進制碼加進了真實的物理內存了,關於分析可執行文件格式與物理地址的轉換,不同的CPU與操作系統的實現方式會有不同之處

接下來我們開始實踐部分,首先寫一個求一組數的最大值的彙編程序max.s。

 現在有一個max.o目標文件,我們用readlf工具讀取其ELF Header和Section Header Table

 

ELF Header中描述了操作系統是UNIX,體系結構是80386。Section Header Table中有8個Section Header,從文件地址200
(0xc8)開始,每個Section Header佔40字節,共320字節,到文件地址0x207結束。這個目標文件沒有Program Header。文件地址是這樣定義的:文件開頭第一個字節的地址是0,然後每個字節佔一個地址。

 

從Section Header中讀出各Section的描述信息。
Addr是這些段加載到內存中的地址(程序中的地址都是虛擬地址),加載地址要在鏈接時填寫,現在空缺,所以是全0。
OffSize兩列指出了各Section的文件地址,比如.data段從文件地址0x60開始,一共0x38個字節,回去翻一下程序,.data段定義了14個4字節的整數,一共是56個字節,也就是0x38。

 

根據以上信息可以描繪出整個目標文件的佈局。


 

** Section Header Table**:讀出各Section的描述信息。
.shstrtab:保存着各Section的名字,比如.text,.data.....。
.strtab:保存着程序中用到的符號的名字.比如彙編程序的start_loop:和loop_exit符號。(對應的就是for循環)。
**.data **:保存程序中已初始化的全局變量和靜態變量以及字符串常量。
.bss:存放程序中未初始化的全局變量和靜態變量。
.text:存放程序執行代碼。
.rel.text:告訴鏈接器指令中的哪些地方需要做重定位。
下節分析。

 我們看一下.text段內容

 text段代碼中,一些跳轉指令和內存訪問指令中的地址都是符號的相對地址,下一步鏈接器要修改這些指令,把其中的地址都改成加載時的內存地址,這些指令才能正確執行。

2.2可執行文件

現在分析可執行文件max。

 

 

 

在ELF Header中,Type改成了EXEC,由目標文件變成可執行文件了多了兩個Program Header,少了兩個Section Header。

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

多出來的Program Header Table描述了兩個Segment的信息。.text段和前面的ELF Header、Program Header Table一起組成一個Segment(FileSiz指出總長度 是0x9e),.data段組成另一個Segment(總長度是0x38)。VirtAddr列指出第一 個Segment加載到虛擬地址0x0804 8000,第二個Segment加載到地址0x0804 90a0。Flg列指出第一個Segment的訪問權限是可讀 可執行,第二個Segment的訪問權限是可讀可寫。


 

 原來目標文件符號表中的Value都是相對地址,現在都改成絕對地址了。

 我們查看一下.txt段內容。

現在我們對比一下目標文件和可執行文件的不同。

目標文件.text和.data段地址

 可執行文件.text和.data段地址

 目標文件中跳轉指令

 可執行文件中跳轉指令

 目標文件中內存訪問指令

可執行文件中內存訪問指令 

 

  • 可以看到指令中的相對地址都改成絕對地址了。
  • 結合上2部分分析,我們可以看到。
  • .text和.data段代碼加載到內存中的地址由空缺0變成了具體地址。
  • .text段代碼中一些跳轉指令和內存訪問指令中的地址由相對地址改成加載時的內存地址,
  • .data段代碼也由相對地址改爲絕對地址。

3.靜態庫和共享庫

:有時候需要把一組代碼編譯成一個庫,這個庫在很多項目中都要用到,例如libc就是這樣一個庫,我們在不同的程序中都會用到libc中的庫函數(例如printf)。

共享庫和靜態庫的區別:在鏈接libc共享庫時只是指定了動態鏈接器和該程序所需要的庫文件,並沒有真的做鏈接,可執行文件調用的libc庫函數仍然是未定義符號,要在運行時做動態鏈接。而在鏈接靜態庫時,鏈接器會把靜態庫中的目標文件取出來和可執行文件真正鏈接在一起。

  • 靜態庫鏈接後,指令由相對地址變爲絕對地址,各段的加載地址定死了。
  • 共享庫鏈接後,指令仍是相對地址,共享庫各段的加載地址並沒有定死,可以加載到任意位置。

靜態庫好處:靜態庫中存在很多部分,鏈接器可以從靜態庫中只取出需要的部分來做鏈接 (比如main.c需要stach.c其中的一個函數,而stach.c中有4個函數,則打包庫後,只會鏈接用到那個函數)。另一個好處就是使用靜態庫只需寫一個庫文件名,而不需要寫一長串目標文件名


 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章