section header table 是一个section header的集合,每个section header是一个描述section的结构体。在同一个ELF文件中,每个section header大小是相同的。
每个section都有一个section header描述它,但是一个section header可能在文件中没有对应的section,因为有的section是不占用文件空间的。每个section在文件中是连续的字节序列。section之间不会有重叠。
一个目标文件中可能有未覆盖到的空间,比如各种header和section都没有覆盖到。这部分字节的内容是未指定的,也是没有意义的。
section header结构体的定义可以在 /usr/include/elf.h 中找到。
/* 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;
下面我们依次讲解结构体各个字段:
(1)sh_name,4字节,是一个索引值,在shstrtable(section header string table,包含section name的字符串表,也是一个section)中的索引。第二讲介绍ELF文件头时,里面专门有一个字段e_shstrndx,其含义就是shstrtable对应的section header在section header table中的索引。
(2)sh_type,4字节,描述了section的类型,常见的取值如下:
(3)sh_flags, 32位占4字节, 64位占8字节。包含位标志,用 readelf -S 可以看到很多标志。常用的有:
(4)sh_addr, 对32位来说是4字节,64位是8字节。如果section会出现在进程的内存映像中,给出了section第一字节的虚拟地址。
(5)sh_offset,对于32位来说是4字节,64位是8字节。section相对于文件头的字节偏移。对于不占文件空间的section(比如SHT_NOBITS),它的sh_offset只是给出了section逻辑上的位置。
(6)sh_size,section占多少字节,对于SHT_NOBITS类型的section,sh_size没用,其值可能不为0,但它也不占文件空间。
(7)sh_link,含有一个section header的index,该值的解释依赖于section type。
(8)sh_info,存放额外的信息,值的解释依赖于section type。
(9)sh_addralign,地址对齐,如果一个section有一个doubleword字段,系统在载入section时的内存地址必须是doubleword对齐。也就是说sh_addr必须是sh_addralign的整数倍。只有2的正整数幂是有效的。0和1说明没有对齐约束。
(10)sh_entsize,有些section包含固定大小的记录,比如符号表。这个值给出了每个记录大小。对于不包含固定大小记录的section,这个值是0。
系统预定义了一些节名(以.开头),这些节有其特定的类型和含义。
示例:从一个ELF文件中读取存储section name的字符串表。前面讲过,该字符串表也是一个section,section header table中有其对应的section header,并且ELF文件头中给出了节名字符串表对应的section header的索引,e_shstrndx。
我们的思路是这样:
从ELF header中读取section header table的起始位置,每个section header的大小,以及节名字符串表对应section header的索引。
计算section_header_table_offset + section_header_size * e_shstrndx 就是节名字符串表对应section header的偏移。
读取section header,可以从中得到节名字符串表在文件中的偏移和大小。
把节名字符串表读取到内存中,打印其内容。
代码如下:
/* 64位ELF文件读取section name string table */
#include
#include
#include
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,则它没有名称,或名称是空。