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