.obj是目標文件,所以可以知道目標文件是指編譯后生成的文件,目標文件幾乎和可執行文件相同只是稍微有點不同而已。其不同之處在于有些符號和地址沒有被調整。
正是因為目標文件與可執行文件幾乎相同,所以它們的存儲格式是一樣的,可以把它們近似看成同一種文件。
Linux下的動態鏈接庫格式為.so,Windows和Linux下的靜態鏈接庫格式分別為.lib和.a。
靜態鏈接庫是一個文件,該文件包含了很多目標文件,它是一個整體。
Linux下的可執行文件是按照ELF格式存儲的,ELF標準包含4種文件,請看P81。我所熟悉的Windows下的DLL就屬于共享目標文件。
目標文件一般包含了哪些內容?編譯后的機器指令代碼、數據、連接所需的信息、符號表、調試信息、字符串等。
目標文件把信息按照屬性的不同分段存儲。寫到這里我感覺這書上說的與老師課上講的程序在內存中的分段方法有些相似。在目標文件中,編譯后的機器指令代碼放在代碼段(Code
Section)中,段名一般為.code和.text。全局變量和靜態變量放在數據段(Data
Section)中,段名一般為.data。
BSS段(Block Started By Symbol)用來存儲未初始化的靜態變量和全局變量。話雖如此bss中并沒有這些變量的內容,它只是為這些變量按照所占空間大小預留空間而已。由于這些變量默認就是0,所以壓根沒必要再為它們分配一個數據0,也沒有必要讓它們待在data段中。因此bss的作用是為這些變量預留空間。
另外目標代碼還有一個文件頭用來保存該目標文件的信息,它里面還有一個段表。
源代碼被編譯以后生成兩種段數據段和指令段,.code.text屬于指令段.data.bss屬于數據段。
這樣分主要有3點好處:
1、防止程序被有意無意篡改。這是因為指令段只讀,數據段可讀寫。
2、提高了緩存命中率。
3、節省內存空間。因為指令段可被多個副本共享,但是副本可以擁有自己的數據段。
原來目標文件中的段還有只讀數據段(.rodata)、注釋信息段(.comment)、堆棧提示段(.note.GNU-stack)。
從書中所給的例子來看一個ELF文件只有4個段是由內容的,即.data、.text、.rodata、.comment。
從圖3-3可以看出在內存中,從低地址到高地址是按照ELF
header、text、data、rodata、comment、other
data的順序存放的。
由本小節可知,全局變量可能因為語言和編譯器的不同不一定存放在bss段,但是靜態變量一定存放在bss段。
雖說bss存放的是未初始化的靜態和全局變量,但是有些變量如果被初始化為0,它也會被放在bss中,這是編譯器的優化,有時候這種優化會帶來麻煩。
表3-2列出了其他段及意義。
此外,這個段還可以自定義。
圖3-4展示了ELF的層次結構。
最重要的兩個部分就是ELF文件頭和段表。ELF文件頭描述整個文件的基本屬性,段表描述各段的信息。
清單3-2清楚地描述了ELF文件頭的信息,P95黑體部分列舉了ELF文件頭包含的信息。
ELF文件兼容各平臺,它的文件結構和相關參數定義在”/usr/include/elf.h”里,它有32位和64位兩種。
表3-3展示了elf.h的自定義變量體系。
表3-4展示了ELF文件頭結構成員含義。
ELF魔數:ELF文件頭的第一個字段是Magic,包含16bytes,對應于Elf32_Ehdr中的e_ident成員。Magic用來表示平臺的各種屬性。
1~4個字節是所有ELF文件都相同的標識碼,分別對應del、E、L、F,這四個字節就是ELF魔數。操作系統通過確認魔術是否正確以決定是否加載可執行文件。
第5個字節用來表示ELF文件是32位的還是64位的。
第6個字節用來表示ELF字節序。
第7個字節用來表示ELF文件版本號。
后面的9個字節用來預留,有些平臺可能用來作為擴展標志。
Elf32_Ehdr中的e_type成員表示ELF文件類型,ELF總共有三種文件類型如表3-5所示。操作系統是通過判斷文件類型而不是擴展名來確定ELF文件類型的。
Elf32_Ehdr中的e_machine成員表示ELF文件的平臺屬性。雖然ELF遵循統一標準但不代表同一ELF文件可以在不同平臺上使用。
它用來表示各個段的信息,ELF文件中的段是由段表決定的。
一個ELF文件不僅僅包含像data、text、bss這樣的段,還包括其他的輔助性段。
段表是一個Elf32_Shdr類型的結構體數組,元素的個數代表段的個數,每個元素對應一個段。這個Elf32_Shdr被稱為段描述符。
表3-7描述了Elf32_Shdr中各字段的意義。
段的名稱對于編譯和鏈接有意義,對操作系統無意義。決定段的類型的是段的類型字段,并不是段的后綴名和名稱。
段的類型和段的標志位字段決定了段的屬性。表3-8展示了段的各種類型。
段的標志位表示該段在進程虛擬地址空間中的屬性,如是否可讀。表3-9列出了段的各種屬性。
表3-10列出了系統保留段的各種屬性。
段的連接信息包括sh_link和sh_info,它們與鏈接相關,如表3-11所示。
目標文件中有一個SHT_REL的.rel.text字段,它是重定位表。重定位發生在連接的過程中,這個在前面已經講過,重定位表記錄了重定位相關信息。
顧名思義,就是用來表示各種名稱的字符串的表。它是一個裝有各種字符串的表格,每個字符在表中都有一個固定的位置。
這種表在ELF文件中保存為2種形式——.strtab和.shstrtab,它們分別是字符串表和段字符串表,它們在ELF文件中都以獨立的段而存在。為了輕松地找到這個段,在ELF文件頭中包含了這兩個段的下標,名為e_shstrndx。
鏈接是組合目標文件的過程,目標文件是根據彼此之間的地址相互引用,從而組合成可執行文件的。而,這個地址可以簡單地理解為目標文件中的函數和變量。在這里,函數和變量統稱為符號,函數名和變量名統稱為符號名。
鏈接器的著眼點主要在定義在本目標文件和定義在其他目標文件的全局性符號,因為只有這些涉及到目標文件之間的組合。
ELF文件的符號表是一個段,段名為“.symtab”,它是一個Elf32_sym類型的數組,每個數組元素代表一個符號。
在Elf32_sym結構體中有一個32bit成員叫st_info,低4bit表示符號的類型,高28bit符號的綁定信息。綁定信息具體可見表3-15,符號類型可參見表3-16。
Elf32_sym.st_shndx:如果符號定義在本目標文件中,它表示該符號所在的段在段表中的下標,否則它具有其他意義。st_shndx具體信息可見表3-17。
Elf32_sym.st_value:每個符號都有一個對應值,它一般為變量和函數的地址。st_value的意義有如下幾種:
1、如果符號定義在目標文件中,并且它不是COMMON塊類型,則st_value代表符號在段中的偏移。
2、如果符號定義在目標文件中并且是COMMON塊類型,則st_value表示符號的對齊屬性。
3、在可執行文件中st_value表示符號的虛擬地址。
鏈接器本身自帶的,不是你定義的,定義在鏈接腳本中的,但是你可以用的,這樣的符號是特殊符號。它們存在的時機是鏈接器鏈接生成可執行文件時,此時鏈接器會將它們解析成正確的值,
書中P110舉了幾個具有代表性的特殊符號。
本小節明確了函數簽名的概念。
函數簽名:主要是指函數名和參數類型,其次是所在類和命名空間等。它用于區分不同函數。
編譯器和連接器會使用名稱修飾的辦法加工函數簽名使之成為修飾后名稱,在C++中為符號名。
不同的編譯器對函數簽名的修飾方法不同,這導致不同種類的目標文件無法互連。
原來C++編譯器已經默認定義了宏__cplusplus來兼容C語言和C++。
在不同目標文件中含有相同全局性符號定義,這種情況被稱為強符號,它會引起符號重定義。
C/C++編譯器認為未初始化的全局變量是弱符號。
這個強弱符號是可以被定義的,所以強弱之別是根據定義來劃分的,并不針對符號的引用,P117代碼說明了這一點。
鏈接器根據符號的強弱來處理和選擇定義的全局變量:
1、不允許多次定義強符號,否則報錯。
2、同一個符號在各目標文件中出現了多次,但只有一個是強符號,那么編譯器選擇強符號的那個。
3、如果一個符號在所有目標文件中都是弱符號,那么編譯器選擇占用空間最大的一個。由此可見編譯器對于弱符號的選擇并不明顯,所以由弱符號造成的錯誤也相對難以發現。
強引用:目標文件對于非本目標文件的符號引用,在鏈接成可執行文件的過程中,如果找不到該符號的定義,就報未定義錯誤。
弱引用:與強引用差不多,只不過在找不到符號時不報錯。
強弱引用主要用于庫的鏈接。對于未定義的弱引用,編譯器為便于識別把它看作是某一值,一般為0。
弱符號與COMMON塊聯系較密切。
弱引用是可以手動聲明的,如P118第一段代碼所示。
弱符號的作用在于提供一個默認的庫符號,但是當用戶想要自定義該符號的時候,該自定義符號就獲得了更高的優先級。而弱引用的作用在于增強了程序的可擴展性,因為有了弱引用程序功能更強,沒有弱引用程序也能正常運行。
目標文件和可執行文件中都可能保存調試信息,ELF文件采用DWARF格式保存調試信息。
由于調試信息與可執行文件最終結果無關,而且占用大量空間,所以在發布軟件時應該去掉這些調試信息。