Skip to main content

[程序員的自我修養-linker, loader & library] 閱讀筆記(一)

Opass
A life well lived

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
      • 動態連結訊息

系統保留的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 該符號是個文件名稱
  • 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。不同compiler修飾的方法不一樣,gcc和VC++的修飾方法也不同。如此一來就可以解決符號名稱衝突的問題。btw, 對於global variable來說,他也會被修飾。只是值得注意的是:「變量的型別」並沒有被加入到修飾後的名稱。float a和int a兩個修飾後的名字是一樣的。

C++為了要和C兼容,有一個特殊用法叫做extern "C",也就是出現在extern "C"後的東西要被當成C來處理,語法我就不多加敘述了畢竟不是重點,C++的名稱修飾機制會失效。

所以你可能會看到某些library會出現

#ifdef __cplusplus
extern "C" {
#endif
void *memset (void *, int, size_t);
#ifdef __cplusplus
}
#endif

這是為了讓C++ compiler自動加上extern C來達成名稱修飾的功能。

弱符號與強符號​

我們常常會碰到一種情況,就是兩個文件中重複了定義相同名字的符號,連結的時候將會噴錯。有些符號是「Strong Symbo」,有些符號是「Weak Symbol」。對Compiler來說,函數和已經初始化的global variable是Strong Symbol. 未初始化的global variable是Weak Symbol。

我們也可以透過attribute((weak))去定義任何一個強符號為弱符號

重點在於定義,定義的時候其實就決定一個符號是強還是弱 linker會按照下列規則來處理強弱符號

  • 不允許強符號被多次定義,直接噴錯
  • 如果一個符號在某個obj中是強符號,其他都是弱符號,那麼選擇強符號
  • 如果一個符號在所有文件中都是弱符號,那就選擇佔用空間最大的符號。但這很容易陷入debug海,很難發現錯誤。

強引用和弱引用​

在透過linker轉成可執行文件的過程中,所有的符號都要被正確的resolution。如果沒有找到該符號的定義,就會噴未定義錯誤。這種被稱為Strong Reference。另外與之相對的是Weak Reference,如果該符號有定義,則Linker會將該符號連起來,如果未定義,則Linker不報錯,默認一個特殊的值像是0之類的。

這有什麼好處?

舉例來說,library所定義的弱符號可以被user定義的強符號蓋過。讓程式可以執行字定義版本的library函數。或者你也可以讓一個程式設計成可以自行判斷single thread 或multi thread. 就可以通過弱引用的方法來在執行時動態判斷是否有link到pthread library,決定要跑multi thread版本還是single thread版本。

DEBUG訊息​

compile 時加上gcc -g就會在obj file裡面加入很多debug相關的section。現在的ELF文件採用一個叫做DWARF(Debug With Arbitrary Record Format)的標準debug訊息格式。debug訊息通常會佔用很大的空間,通常是比你本身的code大好幾倍。所以你要release的時候可以用strip command來去掉ELF文件中的debug訊息。

指令列表​

file​

可以使用file命令來查看哪種格式

~/embedded/freertos-plus/build$ file main.elf main.elf: ELF 32-bit LSB executable, ARM, version 1 (SYSV), statically linked, not stripped

gcc -c xxx.c**​

會產生xxx.o,代表只compile不link

objdump​

  • -h xxx.o 列出section header
  • -s Display the full contents of any sections requested. By default all non-empty sections are displayed.(顯示所有非空的section內容)
  • -d Display the assembler mnemonics for the machine instructions from objfile. This option only disassembles those sections which are expected to contain instructions.(反組譯obj file的machine instruction)

size xxx.o​

列出section size和total size

text data bss dec hex filename
88 4 0 92 5c test.o`

readelf​

-h xxx.o 查看ELF檔案的header

Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 312 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 10

-S xxx.o​

Displays the information contained in the file's section headers, if it has any.(列出所有section header)

-s xxx.o​

列出符號表

Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 a
10: 0000000000000000 29 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf

nm xxx.o​

list symbols from object files

該文章hapackd筆記連結:https://embedded-note.hackpad.com/-linker-loader-library-cTe11h7V3KW