程序員的自我修養三目標文件里有什麼
編譯器編譯源代碼後生成的文件叫做目標文件。
目標文件從結構上講,它是已經編譯後的可執行文件格式,只是沒有經過鏈接的過程。
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
",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 調試信息
目標文件裡面還有可能保存調試信息。
編譯器提前將源代碼與目標代碼之間的關係保存到目標文件中。
※ARP與RARP協議及arp腳本
※spring Bean類自動裝載實現
※whatwg-fetch源碼分析
※一篇關於Python裝飾器的博文
TAG:科技優家 |
※論一個非標自動化機械工程師的自我修養
※杠精的自我修養
※論輔導員的自我修養
※一片麵包的自我修養
※論「杠精」的自我修養
※共產黨員要用良好的自我修養錘鍊黨性
※文藝「鏟屎官」的自我修養
※一款車機系統的「自我修養」
※一個「杠精」的自我修養
※一個人有無文化修養的檢驗標準
※這些書畫作品 提升了你的文化修養
※論偶像的自我修養
※不同品牌護膚油的自我修養
※顏值真的那麼重要嗎?談一談一個演員的自我修養的必要性
※一條鯰魚的自我修養
※書法鑒賞是國民必具的文化修養之一
※婁藝瀟「懟」閨蜜程瀟全程素顏,請問你們女藝人的自我修養在哪裡?
※一款專業運動耳機的自我修養
※一名「被拍者」的自我修養
※書法人的自我修養