[程序員的自我修養-linker, loader & library] 閱讀筆記(一)
chapter 3: talk about object file
object file的分類
現在Linux和Windows上的可執行檔,基本上是基於COFF格式演變而來。COFF(Common file format)是Unix System V Release 3首先提出的格式規範。之後被微軟和Linux拿去參考最後定義出執行檔格式。
COFF最重要的觀念在於引入了section的機制,讓不同的obj file可以擁有不同的section。廣義上來看,Obj file的格式基本上就是COFF的變體。
- Windows上叫做PE(Portable Executable)
- Linux上叫做ELF(Executable Linkable Foramt) 也就是說,基本上你可以把這種文件統稱為:ELF文件
ELF文件大致上可以分成四種類型
- 可重定位文件(Relocatable File)
- Linux的.o檔
- Windows的.obj檔
- 可執行檔(Executable File)
- Linux的可執行檔
- Windows的.exe
- 動態共享函式庫(Shared Obj File)
- Linux的.so檔
- Windows的.dll檔
- CoreDump File
- Linux的coredump file
Object File的格式
關鍵在於Section,一個程式碼Source code檔案被Compile後,會依照指令和資料部份分開存到Obj File上不同的Section中。
先來點概念,一個Obj File的格式大概是長這樣的(當然,這省略的很多細節)
- File Header
- 描述該文件的屬性,例如該文件是否可以執行、是靜態連結還是動態連結、target 硬體、target OS等
- Section Table: 紀錄了該份文件有哪些Section, 有哪些屬性
- .text section
- instruction通常都保存在這裡
- .data section
- 通常用於紀錄初始化的global variable和local static variable
- .bss section
- 通常用於紀錄未被初始化的global variable和static variable,通常只是紀錄符號和留一段空間
BSS的意思
Block Started by Symbol, 最初是某個Assembler的假指令,用於替Symbol預留一段空間。這個Assembler用於美國聯合航空公司1950年代中期。
為什麼要把指令和資料分開存取?
因為有很多好處,最顯而易見的是
- data是可以讀寫的,而指令是唯讀的。也就是你可以再記憶體中分開成唯讀和可寫,這可以防止程式在執行的時候被惡意改寫。
- Cache, 為了提高Cache的命中率。把指令和資料分離有助於提高Cache的命中率
- 這就是改良後的哈佛架構(Modified Harvard architechture),讓指令和資料存在不同的memory address上。好處是可以有效的利用space locality(for data)和time locality(for instruction),有助於cache
- 方便進行fork,這樣的話你指令只要保存一份,不同的資料保存很多份,就不會浪費記憶體。這是個相當重要的作法。
深入觀察一個Object File
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001d 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000060 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000064 2**0
ALLOC
3 .rodata 00000003 0000000000000000 0000000000000000 00000064 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002c 0000000000000000 0000000000000000 00000067 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000093 2**0
CONTENTS, READONLY
6 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
- CONTENTS, READONLY等代表section的屬性
- contents代表在該文件中存在
- Fileoffset的部份代表section的起頭address
- size代表該section的大小
Section說明
要先記得一件事,不同的compiler對於同一份source code的處理方法可能會不同。有些compiler會把字串放到.rodata, 有些會把他放到.data。微軟MSVC和gcc的實作不一樣。另外還會有big-endian或little-endian的問題。
- .data
- 保存已經初始化的全域變數和static變數
- .rodata
- 放唯讀data, 一般來說是const變數和字串。好處是可以在載入的時候把這段section設成唯讀。任何修改操作都視為非法,另外某些嵌入式平台下,有ROM這種記憶體,那麼你把.rodata放在該區域中就可以保證該資料的唯讀性。
- .bss
- 存放未初始化的全域變數和static變數。更精確來說,應該說.bss預留了空間,不同的編譯器實作不同,有些會將未初始化的全域變數放到.bss, 有些又不放,只預留一個未定義的全域變數符號在symbol table(這後續再提),等到最後link時再分配空間。不過簡單起見,原則上你還是可以說.bss段是用來放未被初始化的全域和static變數。
- 其他Section(還有很多未被列出)
- .comment
- 存放編譯器的版本訊息,像是"GCC:4.8.1...."之類
- .hash
- .debug
- .note
- 額外的編譯訊息
- .symtab
- 符號表
- .strtab
- 字串表
- .dynamic
- 動態連結訊息
- .comment
系統保留的section名稱前面會有dot符號,你當然可以自己定義Section name放進去objfile中,透過 __attribute__((section("FOO"))) int global = 42
就可以把對應的變數或函數放到指定的section中。
ELF文件結構
大致上,ELF文件會有下列部份
- ELF Header
- 描述了ELF文件版本、target machine型號、程序入口地址等
- .text
- .data
- .bss
- ...other section
- Section header table
- 描述了每個Seciton的名稱、長度、offset,、讀寫權限、其他屬性
- string tables
- symbol tables
- ...
ELF有32和64位元的版本。差別只是在於某些描述的成員型別大小不一樣。可以大概了解ELF header裡面到底描述了什麼(用readelf -h xxx.o)
- ELF Magic Number
- 有16個bytes
- 最開始一定是7f 45 4c 46, in ascii,其實是指 7f 45(E) 4c(L) 46(F)
- a.out的格式開頭一定是0x01, 0x07,這是有歷史原因的,因為PDP的jump指令和向後兼容的關係。
- 幾乎所有的可執行文件一開始都是magic number, 用來讓機器判斷文件類型。
- 第五個byte代表這是32bit(1)還是64bit(2)的ELF檔
- 第六個byte代表big endian還是little endian
- 第七個byte代表版本號,一般來說是1,因為ELF目前是1.2版,自從1995年後就沒有update了。
- 後面其他九個bytes未定義,一般來說是0。
- Class(32還是64bit的版本)
- Data: 2補數, little endian
- Version: 1
- ABI Version: 0
- OS: Unix System V
- machine: Advanced Micro Devices X86-64
- Type:REL (Relocatable file)文件類型
- 有REL(relocation .o文件), EXEC(execution), DYN(synamic .so文件)三種
- entry point address: 系統load完之後,會從這裡開始執行指令。一般來說可重定位文件是沒有入口位址的,也就是這個值為0
Section Header
底層其實是一個描述某個section的struct。叫做Elf32_Shdr
(32位元),你可以打開/usr/include/elf.h
看到。
基本上,描述了
- Section name段名
- Section Type段的類型, 常見的有
- NULL無效段
- PROGBITS 程序段,放code的地方
- SYMTAB 符號表
- STRTAB 字串表
- REL 重定位訊息,具體請參考靜態地址resolution和重定位
- NOBITS 表示該段沒有內容,像是.bss段
- DYNAMIC 動態連結
- HASH 就是個hashXD
- Section flag段的flag, 代表該段再process虛擬地址空間中的屬性,是否可寫、是否可執行
- WRITE表示該段在process空間中可寫
- ALLOC表示該段再process中必需要分配空間
- EXECINSTR 表示該段在process空間中可以被執行
- addr 段的虛擬地址
- offset 段的偏移量
- size 段的長度
- link info 段的連結訊息
- addralign 段的地址對齊
- entsize: entry size的長度,某些段像是符號表,每個符號的entry size是一樣的。要紀錄。如果是0代表該段不包含固定大小的entry。
連結
基本上,連結的關鍵在於「符號」,就像積木一樣把定義和使用結合在一起。連結很重要的關鍵在於符號的管理。每個obj file都會有一個相對硬的Symbol Table,這個表紀錄了Obj file中用到的所有符號。每個定義的符號都有一個對應值,稱為Symbol Value,對於variable和functoin來說,Symbol Value指的就是他們的地址。除了variable和function之外,還有幾種不常用到的符號:
符號類型
- global variable,可被其他obj file引用
- 外部符號,你有使用,但是卻沒有定義在自己的obj file中
- 段名,這個符號的值就是該section的start address
- 局部符號,像是static variable, 這類的符號對linker來說意義不大,linker往往忽略他們。
- 行號信息,這是optional
我們可以使用nm指令來查看符號表
符號表結構
當然,一個符號的結構其實是定義在某個header file的struct裡面,每個符號表中的符號紀錄了下列資訊
- name 符號名稱
- value 符號的值
- 不同符號的值是不同的,可能是一個值,或是一個address
- size 符號佔據的空間大小
- info 和符號相關的綁定訊息,其中包含了[binding info][type info]
- binding info
- LOCAL local符號,obj外部不可見
- GLOBAL global符號,外部可見
- WEAK 弱引用,詳情見「強符號弱符號」
- type info
- NOTYPE 未知類型
- OBJECT 是個variable
- FUNC 是個function或可執行代碼
- SECTION 該符號是個section
- FILE 該符號是個文件名稱
- binding info
- other 沒用
- shndx 符號所在的段
- 如果符號定義在本obj file,那麼這個成員表示的符號就在symbol table中
- 反之
- ABS 代表該符號包含了一個絕對值,像是表示文件名的符號
- COMMON 表示該符號是一個COMMON block類型的符號(見深入靜態連結之COMMON block)
- UNDEF 該符號未定義,代表該符號在本obj file被引用到,但是定義在其他obj文件中。
關於符號
古早的時候,編譯器產生出的obj file是沒有修飾的,隨著library發展,大家發現程式寫一寫會不小心和library衝突。於是就有人想到了請compiler把所有library內的符號前面加上,像是foo經過C compiler後就會變成_foo,不過這只不過是個應急的策略,根本上並沒有解決問題。後來C++提出了namespace的功能。
C++語法既強大,又複雜。光是overloading就可以搞死人了。最簡單的粒子就是func(int)和func(float)。為了要區分這種情況,compiler內部出現了一種機制叫做「name mangling或是name decoration」。
name decoration會分析一個函數的名稱、參數、回傳值、名稱空間等訊息,最後產生出一個獨一無二的符號。舉例來說,int C::C2::func(int)
會變成_Z1C2C24funcEi