ELF檔案
本文主要針對Linux系統。在x86架構下,Linux使用的是ELF(Executable and Linkable Format)目标檔案格式。目标檔案的三種格式:
- Relocatable object file. 可重定位目标檔案包含二進制代碼和資料,編譯時可與其他可重定位目标檔案合并組成可執行目标檔案,如
,.o
檔案。.a
- Executable object file. 可執行目标檔案包含二進制代碼和資料,可以直接加載到記憶體并執行, 如
檔案。.out
- Shared object file. 共享目标檔案是一種特殊類型的可重定位目标檔案,可以在加載或運作時被動态的加載到記憶體并連結。如
檔案。.so
Relocatable object file
編譯器和彙編器生成可重定向(或共享)目标檔案,連結器生成可執行目标檔案。ELF header 以一個16位元組的序列開始的,描述了生成改檔案的系統的字的大小(word size)和位元組序(byte ordering),剩下的ELF header則包含了一些讓連結器去parse和interpret目标檔案的資訊,如ELF頭的大小,目标檔案類型(Relocatable,executalbe, or shared), 及其類型(eg., IA32), section header table的檔案偏移,section header table中條目大小和數目。不同節的位置和大小是由節頭部表描述的, 且每個節都有個固定大小的條目。一個典型的Relocatable object file如下圖:

其中各部分解釋如下:
- .text: 編譯好的機器代碼。
- .rodata: read-only資料,比如常量字元串。
- .data: 初始化過的全局變量。
- .bss: (block storage start)未初始化的全局比那裡, better save sapce :),僅僅是占位符,未初始化不需要占據實際磁盤空間。
- .symtab: symbol table, 函數與全局變量。
- .rel.text: 一個 .text 節中位置的清單,連結器把其和其他可重定位檔案連結時,會修改這些位置。通常調用外部函數或引用外部全局變量需要修改,本地則不需要。Executable目标檔案則不需要這些資訊,一般不顯示制定就預設。
- .rel.data: 被子產品引用或定義的全局變量的重定位資訊,比如初始化的全局變量初始值是外部的一個全局變量或函數的位址。
- .debug: 調試符号表,包含了局部變量和類型資訊,引用或定位的全局變量,還有原始的C檔案,編譯加 -g 選項才此表。
- .line: 源代碼行号和.text中機器碼的映射關系,編譯時加 -g 才有此資訊。
- .strtab: string table, 包含了 .symtab 和 .debug中的符号表,還有節頭部的節名字,是以
結尾的字元串序列。null
c語言是分離式編譯,每個
.c
可以程式設計一個
.o
,是一個編譯單元。在連結器上下文中,static符号不對外部子產品可見。
符号表由彙編器生成,是一個數組,裡面的條目由下面結構體所描述。name是字元串表中自己偏移。value是符号的位址,對于 executable 檔案就是運作時絕對位址。type是函數或資料。binding表示是本地還是全局。連結器解析全局符号時,隻允許有一個強符号(已經初始化的全局變量是強符号,反之弱符号)。
typedef struct {
int name; /* string table offset */
int value; /* section offset, or VM address */
int size; /* object size in bytes */
char type:, /* data, func, section, or src file name (4 bits) */
binding:; /* local or global (4 bits) */
char reserved; /* unused */
char section; /* section header index, ABS, UNDEF, */
/* or COMMON */
} Elf_Symbol;
連結器連結靜态庫
.a
時有一些注意點。符号解析階段,連結器按照從左到右的順序來掃描指令行上輸入的
.o
和
.a
檔案。這些檔案在指令行上的順序要注意,庫
.a
一般放到結尾。如果引用的庫還不是互相獨立的,那麼要求有序放置,要有定義在引用之後,也即a依賴b,那麼b放到a後面。還有一種方法是把互相依賴的連結庫合并。連結器完成了符号解析後,最後要做的是重定位,合并輸入子產品,并且為每個符号配置設定運作時位址。重定位有兩部:重定位節和符号定義;重定位節中的符号引用。
Executable Object Files
可執行目标程式結構類似于可重定位目标程式,沒有了重定位資訊.rel節。ELF header還包括了程式的entry point,即程式運作時執行的第一條指令位址。
.init
節定義了一個小函數
_init
,程式的初始化代碼會調用它。在指令行啟動一個可執行程式,通過調用execve核心系統調用調到作業系統loader代碼,把代碼和資料讀到記憶體,跳轉到第一條指令entry point來運作它,拷貝的過程叫加載(loading),最終會調用程式的main函數。一個ELF的結構圖如下:
程式被加載運作,必然是在一個程序上下文中,有自己的虛拟位址空間。父程序
fork
一個子程序,然後通過
execve
系統調用加載器代碼删除子程序已存的虛拟存儲段,加載新的代碼資料與堆棧,新的堆棧段會被初始化為0。32位系統的程序虛拟位址空間如下圖:
Dynamic Linking with Shared Libraries
共享庫
so
在運作期加載到記憶體,并和一個程式連結起來,即dynamic linking, 是由dynamic linker動态連結器來執行的。編譯動态連結庫so,需要用到編譯器gcc的
-shared -fPIC
參數。
加載時動态連結
建立可執行檔案時靜态執行一些連結,最終程式加載時動态完成連結過程。舉例:
編譯動态連結庫:
gcc -shared -fPIC -o libxxx.so a.c b.c
編譯可執行檔案:
gcc -o a.out main.c ./libxxx.so
其中的
-fPIC
建立(Position-Independent Code, PIC), 位置無關代碼不需要連結器修改就可以在任何位址加載和執行這些代碼。在程序虛拟位址空間中,資料段總是在代碼段之後,代碼段中的指令位址和資料段中全局變量位址之間距離是一個運作時常量。正好PIC資料引用可以利用此點,編譯器在資料段開始的地方建立了一個全局偏移量表(Global offset Table,GOT), 每個被目标子產品引用的全局變量都有一個條目,編譯時還為每個條目生成一個重定位記錄。加載時,Dynamic Linker就會重定位每個條目,使其包含正确的運作時絕對位址。
運作時動态連結
程式在運作時完成動态連結,動态加載so,可以使用下面的API. 這種方法使用的更加廣泛寫。API可以在include目錄下搜尋
[email protected]:/usr/include# grep dlopen -r *
, 很容易找到。
/* Open the shared object FILE and map it in; return a handle that can be
passed to `dlsym' to get symbol values from it. */
extern void *dlopen (const char *__file, int __mode) __THROWNL;
/* Unmap and close a shared object opened by `dlopen'.
The handle cannot be used again after calling `dlclose'. */
extern int dlclose (void *__handle) __THROWNL __nonnull (());
/* Find the run-time address in the shared object HANDLE refers to
of the symbol called NAME. */
extern void *dlsym (void *__restrict __handle,
const char *__restrict __name) __THROW __nonnull (());
ELF常用處理指令
- nm,列出符号表
- readelf, 讀ELF檔案完整結構
- objdump, 顯示ELF所有資訊,常用來反彙編.text中指令
- ldd,列出executable object file運作時需要的動态庫
- strings,列出所有可列印的字元串
- size, 列出目标檔案中節的名字和大小
- ar, 建立靜态庫,插入、删除、列出和提取成員
對于ELF這種二進制檔案,同樣可以用grep來搜裡面的字元串,如
grep -ao a.out
可以搜裡面的常量字元串和符号表,有時候也是很友善的。
參考
Computer Systems: A Programmer’s Perspective (3rd Edition)