從C代碼到ELF檔案格式(1) – 了解ELF
現在是網際網路時代,程式設計語言更多考慮的是表達能力,而非單純的指令級性能。是以,程式設計語言越來越向動态語言,腳本語言發展。但是,無論程式設計語言怎麼發展,基本的邏輯還是不能變的,了解elf檔案格式對提升水準還是很有用的。從小處說,學基礎的東西可以以不變應萬變;說得遠一點,相信随着國家的發展,越來越多的基礎研發工作會轉移過來,相應的工作也許會多起來吧,加油!本系列文章,主要讨論C和elf格式的對應關系。主要涉及C的編譯和連結的具體關鍵細節,看似偏了點,但其實本應該是專業程式員的常識。
上過高等工科課程的同學一般都學習過C程式設計語言,雖然不是所有人都會在工作中用到。開源的 Linux 作業系統主要是用C語言編寫的(少量彙編)。linux上(以及其他unix系OS)的可執行程式,lib庫(動态和靜态庫), 包括linux核心鏡像本身都是使用的elf檔案格式。雖然windows使用PE格式,但PE檔案格式和elf檔案格式有個共同祖先——COFF檔案格式,是以它們的很多表示方式是一緻的。
本文先簡單分析下C的程式結構,elf檔案格式的基本組成,C程式的編譯連結過程,然後考慮C程式的彙編輸出形式,目标檔案格式,so檔案格式,靜态庫,程式的加載和動态連結過程,以及他們和C程式的對應關系。所有讨論的概念和示例都是基于linux。
C程式的結構
C程式源碼一般是由
*.h *.c
檔案構成的。
*.h
檔案是頭檔案,包含了需要導出和導入的函數,變量,類型定義,宏定義。
*.c
則是具體的變量定義,函數實作,和子產品内部類型定義。一般來說,一個
c
檔案會對應一個
h
檔案,共同構成一個可單獨編譯的子產品。編譯器會一個一個
c
檔案的編譯,每個
c
檔案會生成一個目标檔案
*.o,
然後根據連接配接腳本把多個
*.o
連接配接成可執行程式或者庫檔案(例如
*.so
)。
a.c + a.h -> a2.c -> a.o
+ = exe或動态庫(*.so)或靜态庫(*.ar)
b.c + b.h -> b2.c -> b.o
C語言有個很奇怪的特性,叫做宏。宏本身也是一種語言,每行都以#開頭,在被編譯器處理之前,先由預處理器,處理宏語言來完成宏替換,代碼塊的選擇,頭檔案的包含。預處理之後才是真正純粹的C代碼。最後編譯器其實隻用處理一個個單獨的C檔案即可。
預處理完之後的C檔案有哪些内容呢?舉個例子:
1. 需要導入的外部函數,全局變量。 extern
2. 需要導出的函數的聲明和定義,變量的聲明和定義。 extern
3. 内部使用的全局變量, 函數。static
4. 類型定義。編譯完成後,這部分資訊elf中不存在。
了解ELF檔案
ELF(Executable and Linking Format),最初由UNIX實驗室開發的,是應用程式接口(ABI)的一部分。它是Object檔案格式,分為三種:
可重定位檔案(relocatable file ): 包含代碼和資料,可與其他對象檔案連接配接成可執行檔案或共享庫。
可執行檔案(executable file): 包含可以執行的代碼。
共享對象檔案(shared object file)首先,它可以和其他重定位檔案和共享對象檔案連接配接,産生新的對象檔案。另外,可執行檔案可以和它動态連接配接成可執行的鏡像。
ELF檔案格式
如下圖,對象檔案參與程式的連接配接和執行。Linking view顯示了對連接配接(Relocatable)其有效的視圖。Excution view則是最後可執行檔案(loadable)的視圖。

ELF header
在檔案最開頭, 用來指明檔案類型,指令集,編碼格式(如大小端),檔案的結構即:
section
header和
program heade
r的位置和大小等。
Section Header Table
指定了各個section的位置,大小,名稱等資訊,連接配接時需要。
Section
包含各種對象檔案的資訊,如:資料,指令,符号表,重定位資訊等。
Program header Table
是可執行檔案用的,用于指明哥哥segment的位置等資訊。重定位檔案不需要。
這裡我們重點了解下
Section
和
Section Header Table
。
64bit ELF
格式可參考檔案ELF64。
除了ELF header,program header table,section header table, 其它資訊都包含在section中。
section由Section Header Table來定位,它的每一個表項結構如下:
typedef struct
{
Elf64_Word sh_name; /* Section name *///實際是字元串表的偏移
Elf64_Word sh_type; /* Section type *///類型
Elf64_Xword sh_flags; /* Section attributes *///處理器相關
Elf64_Addr sh_addr; /* Virtual address in memory *///虛拟記憶體位址,在加載到記憶體前為0
Elf64_Off sh_offset; /* Offset in file *///從檔案最開始計算的檔案内部偏移
Elf64_Xword sh_size; /* Size of section *///在檔案内占用的位元組數
Elf64_Word sh_link; /* Link to other section *///具體含義依賴section的類型
Elf64_Word sh_info; /* Miscellaneous information *///具體含義依賴section的類型
Elf64_Xword sh_addralign; /* Address alignment boundary *///對齊邊界,2的整數次方
Elf64_Xword sh_entsize; /* Size of entries, if section has table *///内部表(如重定位表)項數目,某些情況下有效,否則為0
} Elf64_Shdr;
下面是一些常見的section類型,其它資訊詳見[ELF64],不贅述:
可以看到,section包含字元串表,符号表,重定位表,Hash表,動态連結等。
字元串表,存儲了各種字元串,每個字元串都是以0結尾,挨個排列。符号表中不直接存儲字元串,而是通過字元串在字元串表中的偏移整數值來引用字元串,除此之外也指明了符号的類型,大小,值,位于的section的索引。符号分為SECTION(section name),FUNC(函數名),OBJECT(資料對象),FILE(對應的源檔案名)等類型。
重定位表,表項如下:
隻看Elf64_Rel:
r_offset指明了符号對應的變量在object檔案的具體位置,一般對應變量或者函數位址在編譯後的二進制指令的操作數的section内部偏移。通過他能夠找到重定位符号在程式中的位置。
r_info則指明了重定位表對應的符号在符号表中的偏移。即指明是對哪個符号重定位。
Hash表,放在.hash section中。用來加速字元串的查找:
hash表的原理,其實是把字元串轉換為整數hash值,使用這個hash值作為數組偏移(會根據捅的個數取模)來查找到對應的數組元素(這裡成為hash bucket,hash桶),字元串集合的元素都通過hash值計算映射到了桶中。由于不同字元串的hash值會重複,是以會有多個不同的字元串在一個桶中(通過chain把這些字元串連接配接起來)。查找字元串時,先計算hash值,找到桶,再在桶中去比對具體的字元串。桶的總數是可以自定義的,越大桶中重複元素就越少,查找就越快。chain數組的個數實際上等于字元串個數,符号S對應chain[S的符号表索引值], chain數組的元素值是桶中下一個符号的符号表索引。bucket數組的值也是符号表索引值。根據字元串擷取字元串對應的符号表索引,僞碼如下:
sym_table_index = bucket[hash(str) % nbucket];
while(sym_table_index != STN_UNDEF){
temp_str = get_str_by_sym(sym_table_index); //根據符号表找字元串
if ( == strcmp(str, temp_str)){ //就是這個符号
return sym_table_index;
}
sym_table_index = chain[sym_table_index];// 桶的下一個符号表索引
}