前文連接:html
今天咱們講對目標文件(可重定位文件)和可執行文件都很重要的section。數組
咱們在講ELF Header的時候,講到了section header table。它是一個section header的集合,每一個section header是一個描述section的結構體。在同一個ELF文件中,每一個section header大小是相同的。(其實看了源碼就知道,32位ELF文件中的section header都是同樣的大小,64位ELF文件中的section header也是同樣的大小)微信
每一個section都有一個section header描述它,可是一個section header可能在文件中沒有對應的section,由於有的section是不佔用文件空間的。每一個section在文件中是連續的字節序列。section之間不會有重疊。ide
一個目標文件中可能有未覆蓋到的空間,好比各類header和section都沒有覆蓋到。這部分字節的內容是未指定的,也是沒有意義的。工具
section header結構體的定義能夠在 /usr/include/elf.h
中找到。ui
/* Section header. */ typedef struct { Elf32_Word sh_name; /* Section name (string tbl index) */ Elf32_Word sh_type; /* Section type */ Elf32_Word sh_flags; /* Section flags */ Elf32_Addr sh_addr; /* Section virtual addr at execution */ Elf32_Off sh_offset; /* Section file offset */ Elf32_Word sh_size; /* Section size in bytes */ Elf32_Word sh_link; /* Link to another section */ Elf32_Word sh_info; /* Additional section information */ Elf32_Word sh_addralign; /* Section alignment */ Elf32_Word sh_entsize; /* Entry size if section holds table */ } Elf32_Shdr; typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;
下面咱們依次講解結構體各個字段:spa
sh_name
,4字節,是一個索引值,在shstrtable(section header string table,包含section name的字符串表,也是一個section)中的索引。第二講介紹ELF文件頭時,裏面專門有一個字段e_shstrndx
,其含義就是shstrtable對應的section header在section header table中的索引。sh_type
,4字節,描述了section的類型,常見的取值以下:debug
SHT_NULL
0,代表section header無效,沒有關聯的section。SHT_PROGBITS
1,section包含了程序須要的數據,格式和含義由程序解釋。SHT_SYMTAB
2, 包含了一個符號表。當前,一個ELF文件中只有一個符號表。SHT_SYMTAB
提供了用於(link editor)連接編輯的符號,固然這些符號也可能用於動態連接。這是一個徹底的符號表,它包含許多符號。SHT_STRTAB
3,包含一個字符串表。一個對象文件包含多個字符串表,好比.strtab(包含符號的名字)和.shstrtab(包含section的名稱)。SHT_RELA
4,重定位節,包含relocation入口,參見Elf32_Rela。一個文件可能有多個Relocation Section。好比.rela.text,.rela.dyn。SHT_HASH
5,這樣的section包含一個符號hash表,參與動態鏈接的目標代碼文件必須有一個hash表。目前一個ELF文件中只包含一個hash表。講連接的時候再細講。SHT_DYNAMIC
6,包含動態連接的信息。目前一個ELF文件只有一個DYNAMIC section。SHT_NOTE
7,note section, 以某種方式標記文件的信息,之後細講。SHT_NOBITS
8,這種section不含字節,也不佔用文件空間,section header中的sh_offset
字段只是概念上的偏移。SHT_REL
9, 重定位節,包含重定位條目。和SHT_RELA
基本相同,二者的區別在後面講重定位的時候再細講。SHT_SHLIB
10,保留,語義未指定,包含這種類型的section的elf文件不符合ABI。SHT_DYNSYM
11, 用於動態鏈接的符號表,推測是symbol table的子集。SHT_LOPROC
0x70000000 到 SHT_HIPROC
0x7fffffff,爲特定於處理器的語義保留。SHT_LOUSER
0x80000000 and SHT_HIUSER
0xffffffff,指定了爲應用程序保留的索引的下界和上界,這個範圍內的索引能夠被應用程序使用。sh_flags
, 32位佔4字節, 64位佔8字節。包含位標誌,用 readelf -S <elf>
能夠看到不少標誌。經常使用的有:版本控制
SHF_WRITE
0x1,進程執行的時候,section內的數據可寫。SHF_ALLOC
0x2,進程執行的時候,section須要佔據內存。SHF_EXECINSTR
0x4,節內包含能夠執行的機器指令。SHF_STRINGS
0x20,包含0結尾的字符串。SHF_MASKOS
0x0ff00000,這個mask爲OS特定的語義保留8位。SHF_MASKPROC
0xf0000000,這個mask包含的全部位保留(也就是最高字節的高4位),爲處理器相關的語義使用。sh_addr
, 對32位來講是4字節,64位是8字節。若是section會出如今進程的內存映像中,給出了section第一字節的虛擬地址。sh_offset
,對於32位來講是4字節,64位是8字節。section相對於文件頭的字節偏移。對於不佔文件空間的section(好比SHT_NOBITS
),它的sh_offset
只是給出了section邏輯上的位置。sh_size
,section佔多少字節,對於SHT_NOBITS
類型的section,sh_size
沒用,其值可能不爲0,但它也不佔文件空間。sh_link
,含有一個section header的index,該值的解釋依賴於section type。調試
SHT_DYNAMIC
,sh_link
是string table的section header index,也就是說指向字符串表。SHT_HASH
,sh_link
指向symbol table的section header index,hash table應用於symbol table。SHT_REL
或SHT_RELA
,sh_link
指向相應符號表的section header index。SHT_SYMTAB
或SHT_DYNSYM
,sh_link
指向相關聯的符號表,暫時不解。sh_link
的值是SHN_UNDEF
sh_info
,存放額外的信息,值的解釋依賴於section type。
SHT_REL
和SHT_RELA
類型的重定位節,sh_info
是應用relocation的節的節頭索引。SHT_SYMTAB
和SHT_DYNSYM
,sh_info
是第一個non-local符號在符號表中的索引。推測local symbol在前面,non-local symbols緊跟在後面,因此文檔中也說,sh_info
是最後一個本地符號的在符號表中的索引加1。sh_addralign
,地址對齊,若是一個section有一個doubleword字段,系統在載入section時的內存地址必須是doubleword對齊。也就是說sh_addr
必須是sh_addralign
的整數倍。只有2的正整數冪是有效的。0和1說明沒有對齊約束。sh_entsize
,有些section包含固定大小的記錄,好比符號表。這個值給出了每一個記錄大小。對於不包含固定大小記錄的section,這個值是0。系統預約義了一些節名(以.
開頭),這些節有其特定的類型和含義。
SHT_NOBITS
,表示不佔文件空間。SHF_ALLOC
+ SHF_WRITE
,運行時要佔用內存的。SHT_PROGBITS
。SHT_PROGBITS
,標誌爲SHF_ALLOC
+ SHF_WRITE
(佔用內存,可寫)。gdb
等工具調試程序,須要該類型信息,類型爲SHT_PROGBITS
。SHT_DYNAMIC
,包含了動態連接的信息。標誌SHF_ALLOC
,是否包含SHF_WRITE
和處理器有關。SHT_STRTAB
,包含了動態連接用的字符串,一般是和符號表中的符號關聯的字符串。標誌 SHF_ALLOC
SHT_DYNSYM
,包含動態連接符號表, 標誌SHF_ALLOC
。SHT_PROGBITS
,程序正常結束時,要執行該section中的指令。標誌SHF_ALLOC + SHF_EXECINSTR
(佔用內存可執行)。如今ELF還包含.fini_array
section。SHT_PROGBITS
,全局偏移表(global offset table),之後會重點講。SHT_HASH
,包含符號hash表,之後細講。標誌SHF_ALLOC
。SHT_PROGBITS
,程序運行時,先執行該節中的代碼。SHF_ALLOC + SHF_EXECINSTR
,和.fini對應。如今ELF還包含.init_array
section。SHT_PROGBITS
,該節內容是一個字符串,指定了程序解釋器的路徑名。若是文件中有一個可加載的segment包含該節,屬性就包含SHF_ALLOC
,不然不包含。SHT_PROGBITS
,包含符號調試的行號信息,描述了源程序和機器代碼的對應關係。gdb
等調試器須要此信息。Note Section
, 類型SHT_NOTE
,之後單獨講。SHT_PROGBITS
,之後重點講。SHT_REL
, 包含重定位信息。若是文件有一個可加載的segment包含該section,section屬性將包含SHF_ALLOC
,不然不包含。NAME,是應用重定位的節的名字,好比.text的重定位信息存儲在.rel.text中。SHT_RELA
,和.rel相同。SHT_RELA
和SHT_REL
的區別,會在講重定位的時候說明。SHT_PROGBITS
, 包含只讀數據,組成不可寫的段。標誌SHF_ALLOC
。SHT_STRTAB
,包含section的名字。有讀者可能會問:section header中不是已經包含名字了嗎,爲何把名字集中存放在這裏? sh_name
包含的是.shstrtab 中的索引,真正的字符串存儲在.shstrtab中。那麼section names爲何要集中存儲?我想是這樣:若是有相同的字符串,就能夠共用一塊存儲空間。若是字符串存在包含關係,也能夠共用一塊存儲空間。SHT_STRTAB
,包含字符串,一般是符號表中符號對應的變量名字。若是文件有一個可加載的segment包含該section,屬性將包含SHF_ALLOC
。字符串以\0
結束, section以\0
開始,也以\0
結束。一個.strtab能夠是空的,它的sh_size
將是0。針對空字符串表的非0索引是容許的。SHT_SYMTAB
,Symbol Table,符號表。包含了定位、重定位符號定義和引用時須要的信息。符號表是一個數組,Index 0 第一個入口,它的含義是undefined symbol index, STN_UNDEF
。若是文件有一個可加載的segment包含該section,屬性將包含SHF_ALLOC
。從這一講開始,都會有練習,方便咱們把前面的理論知識綜合運用。
下面這個練習的目標是:從一個ELF文件中讀取存儲section name的字符串表。前面講過,該字符串表也是一個section,section header table中有其對應的section header,而且ELF文件頭中給出了節名字符串表對應的section header的索引,e_shstrndx
。
咱們的思路是這樣:
section_header_table_offset
+ section_header_size
* e_shstrndx
就是節名字符串表對應section header的偏移。代碼以下:
/* 64位ELF文件讀取section name string table */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> int main(int argc, char *argv[]) { /* 打開本地的ELF可執行文件hello */ FILE *fp = fopen("./hello", "rb"); if(!fp) { perror("open ELF file"); exit(1); } /* 1. 經過讀取ELF header獲得section header table的偏移 */ /* for 64 bit ELF, e_ident(16) + e_type(2) + e_machine(2) + e_version(4) + e_entry(8) + e_phoff(8) = 40 */ fseek(fp, 40, SEEK_SET); uint64_t sh_off; int r = fread(&sh_off, 1, 8, fp); if (r != 8) { perror("read section header offset"); exit(2); } /* 獲得的這個偏移值,能夠用`reaelf -h hello`來驗證是否正確 */ printf("section header offset in file: %ld (0x%lx)\n", sh_off, sh_off); /* 2. 讀取每一個section header的大小e_shentsize, section header的數量e_shnum, 以及對應section name字符串表的section header的索引e_shstrndx 獲得這些值後,均可以用`readelf -h hello`來驗證是否正確 */ /* e_flags(4) + e_ehsize(2) + e_phentsize(2) + e_phnum(2) = 10 */ fseek(fp, 10, SEEK_CUR); uint16_t sh_ent_size; /* 每一個section header的大小 */ r = fread(&sh_ent_size, 1, 2, fp); if (r != 2) { perror("read section header entry size"); exit(2); } printf("section header entry size: %d\n", sh_ent_size); uint16_t sh_num; /* section header的數量 */ r = fread(&sh_num, 1, 2, fp); if (r != 2) { perror("read section header number"); exit(2); } printf("section header number: %d\n", sh_num); uint16_t sh_strtab_index; /* 節名字符串表對應的節頭的索引 */ r = fread(&sh_strtab_index, 1, 2, fp); if (r != 2) { perror("read section header string table index"); exit(2); } printf("section header string table index: %d\n", sh_strtab_index); /* 3. read section name string table offset, size */ /* 先找到節頭字符串表對應的section header的偏移位置 */ fseek(fp, sh_off + sh_strtab_index * sh_ent_size, SEEK_SET); /* 再從section header中找到節頭字符串表的偏移 */ /* sh_name(4) + sh_type(4) + sh_flags(8) + sh_addr(8) = 24 */ fseek(fp, 24, SEEK_CUR); uint64_t str_table_off; r = fread(&str_table_off, 1, 8, fp); if (r != 8) { perror("read section name string table offset"); exit(2); } printf("section name string table offset: %ld\n", str_table_off); /* 從section header中找到節頭字符串表的大小 */ uint64_t str_table_size; r = fread(&str_table_size, 1, 8, fp); if (r != 8) { perror("read section name string table size"); exit(2); } printf("section name string table size: %ld\n", str_table_size); /* 動態分配內存,把節頭字符串表讀到內存中 */ char *buf = (char *)malloc(str_table_size); if(!buf) { perror("allocate memory for section name string table"); exit(3); } fseek(fp, str_table_off, SEEK_SET); r = fread(buf, 1, str_table_size, fp); if(r != str_table_size) { perror("read section name string table"); free(buf); exit(2); } uint16_t i; for(i = 0; i < str_table_size; ++i) { /* 若是節頭字符串表中的字節是0,就打印`\0` */ if (buf[i] == 0) printf("\\0"); else printf("%c", buf[i]); } printf("\n"); free(buf); fclose(fp); return 0; }
把以上代碼存爲chap3_read_section_names.c
,執行gcc -Wall -o secnames chap3_read_section_names.c
進行編譯,輸出的執行文件名叫secnames
。執行secnames
,輸出以下:
./secnames section header offset in file: 14768 (0x39b0) section header entry size: 64 section header number: 29 section header string table index: 28 section name string table offset: 14502 section name string table size: 259 \0.symtab\0.strtab\0.shstrtab\0.interp\0.note.ABI-tag\0.note.gnu.build-id\0.gnu.hash\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_r\0.rela.dyn\0.rela.plt\0.init\0.text\0.fini\0.rodata\0.eh_frame_hdr\0.eh_frame\0.init_array\0.fini_array\0.dynamic\0.got\0.got.plt\0.data\0.bss\0.comment\0
能夠發現,節頭字符串表以\0
開始,以\0
結束。若是一個section的name字段指向0,則他指向的字節值是0
,則它沒有名稱,或名稱是空。
本章主要講解了section header的定義,各字段含義和可能的取值。而後介紹了系統預約義的一些section名稱。最後咱們綜合運用第二章和第三章的知識,作了一個讀取section names的練習。
下一章咱們將講述符號表和重定位的原理。此係列文章也會在微信公衆號「歡欣之翼」上同步更新,歡迎關注。