程序員的自我修養三目標文件裏有什麽

分類:IT技術 時間:2017-08-10

 

編譯器編譯源代碼後生成的文件叫做目標文件
目標文件從結構上講,它是已經編譯後的可執行文件格式,只是沒有經過鏈接的過程。

3.1目標文件的格式

現在PC平臺流行的是可執行文件格式,主要是win下的PE和linux的ELF,它們都是COFF格式的變種。

不光是可執行文件按照可執行文件格式存儲。動態鏈接庫和靜態鏈接庫文件都是按照可執行文件格式存儲。

 

ELF文件類型說明實例 可重定位文件 包含代碼和數據,可以被用來鏈接成可執行文件或共享目標文件,靜態鏈接庫也是這種 Linux的.0。win的.obj 可執行文件 包含可以直接執行的程序,ELF可執行文件,一般沒有擴展名 win的.exe文件 共享目標文件 包含代碼和數據,可以在兩種情況下使用:1) 鏈接器可使用這種文件和其他可重定位文件和共享目標文件鏈接,產生新的目標文件 Linux的.so,win的DLL 核心轉儲文件 當進程意外終止,系統可以將進程的地址空間的內容以及終止時的其他信息轉儲到核心轉儲文件 Linux下的core dump

3.2目標文件是什麽樣的

目標文件中包含機器指令代碼,數據,還包括裏鏈接時所需要的一些信息:符號表、調試信息、字符串等。目標文件將這些信息按節的形式存儲,也叫做段。

程序源代碼編譯後的機器指令放在代碼段裏(.code .text),全局變量和局部靜態變量數據經常放在數據段(.data),未初始化的全局變量和局部靜態變量一般放在叫(bss)段裏,.bss中只是為未初始化的全局變量和局部靜態變量預留位置,它沒有內容,所以在文件中也不占據空間。
文件頭描述了整個文件的屬性,文件頭還包括一個段表,段表就是一個描述文件中各個段的數組。
程序源代碼被編譯以後主要分成兩種段:程序指令和程序數據。代碼段屬於程序指令,數據段和bss屬於程序數據。

3.3挖掘SimpleSection.o

真正了不起的程序員對自己的程序的每一個字節都了如執掌

/*XimpleSection.c*/
int printf(const char* format,...)

int global_init_var=84;
int global_uninit_var;

void func1(int i)
{
printf("%d\n",i);
}
int main(void)
{
static int static_var=85;
static int static_var2;
int a=1;
int b;

func1(static_var + static_var2 +a+b);

return a;
}

執行 $ gcc -c SimpleSection.c
得到一個1104字節的SimpleSection.o目標文件,使用binutils查看SimpleSection.o內部結構:

上面除了最基本的代碼段、數據段、BSS段以外,還有只讀數據段、註釋信息段、堆棧提示段。
每個段的第二行中“CONTENTS”,”ALLOC”表示段的各種屬性,BSS段沒有CONTENTS,表示它在ELF中沒有內容。

3.3.1 代碼段

3.3.2 數據段和只讀數據段

.data段保存的是那些已經初始化了的全局靜態變量和局部靜態變量。兩個變量共四字節,一共就是data段大小8字節。
字符串常量”%d/n”,被放到了”.rodata”段,”.rodata”段四個字節剛好是字符串常量的ASCII字節序。
“.rodata”段存放的是只讀數據,是程序裏面的只讀變量和字符串常量。

3.3.3  BBS段

.bss段存放的是未初始化的全局變量和局部靜態變量,上述代碼global_uninit_var和static_var2 就被存放在.bss段中。但有些編譯器不會把未初始化的全局變量存放在目標文件.bss段中,只是預留一個未定義的全局變量。但編譯單元內部可見的靜態變量(給global_uninit_var加上static修飾)則是存放在bss段的。

變量存放位置
static int x1=0;//f3放在BBS段中,被認為未初始化的
static int x2=1;

3.3.4 自定義段

GCC提供一個擴展機制,可以指定變量所處的段:

_attribute_((section("FOO"))) int global=42;
_attribute_((section("FOO"))) int FOO()

全局變量或函數之前加上”attribute((section(“name”)))”屬性就可以把相應的變量或函數放到以”name”作為段名的段中。

3.4 ELF文件結構描述

3.4.1 文件頭

可以使用readelf命令來詳細查看ELF文件。

ELF文件有32位版本和64位版本,文件頭結構也有兩個版本,叫做”Elf32_Ehdr”和”Elf64_Ehdr”.文件頭內容是一樣的,不過有些成員大小不一樣。為了對每個成員大小做出明確規定以便於在不同的編譯環境下都擁有相同的字段長度,”elf.h”使用typedef定義了一套自己的變量體系:

ELF文件頭中各個成員含義與readelf輸出結果的對照表:

ELF魔數

幾乎所有可執行文件格式的最開始幾個字節都是魔數,比如a.out開始兩個字節為0X01,0X07。這種魔數用來確定文件類型,操作系統加載可執行文件的時候會確定魔數是否正確。

文件類型

e_type成員表示ELF文件類型。系統通過這個常量來判斷ELF的真正文件類型,而不是通過擴展名。

機器類型

ELF文件格式被設計成可以在多個平臺下使用,但不表示同一個ELF文件可以在不同平臺下使用,是不同平臺下的ELF文件遵循同一套ELF標準。e_machine成員就表示該ELF文件的平臺屬性。

3.4.2 段表

段表就是保存ELF中各種段的基本屬性的結構。它描述了各個段的信息。ELF文件的段結構就是由段表決定的,編譯器,鏈接器和轉載器都是依靠段表來定位和訪問各個段的屬性的。ELF文件中的位置由ELF文件頭的”e_shoff”成員決定。
使用readlf工具查看段表結構:

段表結構是一個以”Elf32_Shdr”結構體為元素的數組。數組元素的個數等於段的個數,每個結構體對應一個段。”Elf32_Shdr”又被描述為段描述符。ELF段表的第一個元素是無效的段描述符,類型為NULL。

段表的位置:

 

段的類型

段的名字只是在鏈接和編譯過程中有意義,它不能真正表示段的類型。

段的標誌位

段的標誌位表示該段在進程虛擬地址空間的屬性,相關常量以SHF_開頭

段的鏈接信息

如果段的類型與鏈接相關,那麽sh_link和sh_info這兩個成員所包含的意義如下圖:(其他類型的段,這兩個成員沒有意義)

3.4.3 重定位表

“.rel.text”的段,類型是”SHT_REL”它是一個重定位表。鏈接器處理目標文件的時候必須對目標文件中某些部位進行重定位,這些重定位信息就存在重定位表中。”.rel.text”就是針對”.text”段的重定位表。

3.4.4 字符串表

因為字符串的長度往往是不定的,所以用固定的結構來表示它比較困難。常見的做法是把字符串集中起來存放一個表,然後使用字符串在表中的偏移來引用字符串。

3.5 鏈接的接口——符號

鏈接過程的本質就是要把多個不同的目標文件之間相互”粘”到一起,為了使不同目標文件之間能夠相互粘合,這些目標文件必須有固定的規則才行。目標文件之間的相互拼合實際上時對地址的引用。鏈接中,我們將函數和變量統稱為符號,函數名或變量名就是符號名
每一個目標文件都有一個對應的符號表,這個表裏面記錄了目標文件所用到的所有符號,每個定義的符號有一個值,這個值叫做符號值
將符號表中所有符號進行分類,它們可能是下面類型中的一種:

  • 定義在本目標恩健的全局符號,可以被其他目標文件引用。
  • 在本目標文件中引用的全局符號,沒有定義在本目標文件內,一般叫做外部符號
  • 段名。這種符號往往由編譯器產生,它的值就是該段的起始地址。
  • 局部符號,這類符號只能在編譯單元內部可見
  • 行號信息,即目標文件指令與源代碼中代碼行的對應關系。

鏈接過程只關系全局符號的相互”粘合”。
使用nm查看符號結果如下:

3.5.1 ELF符號表結構

ELF文件中的符號表往往是文件中的一個段,段名一般叫”.symtab”,每個Elf32_Sym結構對應一個符號。

符號類型和綁定信息

該成員低4位表示符號的類型,高28位表示符號綁定信息。

符號所在段

如果符號定義在本目標文件中,那麽這個成員表示符號所在的段在段表中的下標,如果符號不是定義在本目標文件中或者對於有些特殊符號,st_shndx的值有些特殊。

符號值

每個符號都有一個對應的值。如果這個符號是一個函數或者變量的定義,那麽符號值就是這個函數或變量的地址。

3.5.2 特殊符號

當我們使用id作為鏈接器來鏈接生產可執行文件時,它會為我們定義很多特殊的符號,這些符號並沒有在你的程序中定義,但時你i可以直接聲明並且引用它,我們稱為特殊符號。

3.5.3 符號修飾與函數簽名

編譯器編譯源代碼產生目標文件時,符號名與相應的變量和函數的名字一樣的,將會產生沖突。為了防止沖突,C語言源代碼文件中的所有全局變量和函數經過編譯以後,相對應的符號名前加上下劃線”_”,後來增加了名稱空間的方法來解決多模塊的符號沖突問題。

C++符號修飾

C++符號修飾為了更好的區分兩個函數,看下面代碼:

int func(int);
float func(float);

class C
{
int func(int);
class C2{
int func(int);
};
};

namespace N{
int func(int);
class C{
int func(int);
};
}

函數簽名:函數簽名包含一個函數的信息,包括函數名,參數類型,所在類,名稱空間和其他信息。函數簽名用於識別不同的函數。
在編譯器及鏈接器處理符號時,它們使用某種名稱修飾的方法,使得每個函數簽名對應一個修飾後名稱
上面6個函數簽名在GCC編譯器下,相對應的修飾後名稱如下圖:

GCC的基本C++名稱修飾方法如下:所有的符號都以”_Z”開頭,對於嵌套的名字,後面緊跟”N”,然後時各個名稱空間和類的名字,每個名字前名字字符串長度,再以”E”結尾。參數列表緊跟在”E”後面,對於int類型來說,就是字母”i”。
名稱修飾機制也被用來防止靜態變量的名字沖突。
不同的編譯器廠商名稱的修飾方法可能不同。

3.5.4 extern "C"

C++為了與C兼容,在符號管理上,C++有一個用來聲明或定義一個C符號的”extern C”關鍵字。
用法:

extern "C"{
int func(int);
int var;
}

C++編譯器將在extern “C”的大括號內部的代碼當作C代碼處理。
單獨聲明某個函數或者變量為C語言的符號,使用如下格式:

extern "C" int func(int);
extern "C" int var;

3.5.5 弱符號與強符號

多個目標文件中含有相同名字全局符號的定義,那麽這些目標文件鏈接的時候將會出現符號重復定義的錯誤,這中符號稱為強符號,有些符號可以定義為弱符號。對於C/C++,編譯器默認函數和初始化了的全局變量為強符號,未初始化的全局變量未弱符號。

針對強弱符號的概念,連接器就會按如下規則處理與選擇被多次定義的全局符號:

  • 規則1:不允許強符號被多次定義
  • 規則2:如果一個符號在某個目標文件中是強符號,在其他文件中都是弱符號,那麽選擇強符號
  • 規則3:如果一個符號在所有目標文件中都是弱符號,那麽選擇其中占用空間最大的一個

3.6 調試信息

目標文件裏面還有可能保存調試信息。

編譯器提前將源代碼與目標代碼之間的關系保存到目標文件中。


Tags: 文件 目標 執行 鏈接 轉儲 格式

文章來源:


ads
ads

相關文章
ads

相關文章

ad