ELF文件格式概述(二):section header table

September 25, 2016 at 8:03 pm

那么继续上一篇的话题,我们开始研究section header table。本篇主要有如下小节:

1. section header table
2. string table
3. symbol table
4. 代码段
5. 数据段

section header table

我们之前使用objdump -h命令查看过SimpleSection.o中的一些段信息,但是objdump并不能显示所有的段,现在我们使用readelf -S命令查看其中包含的所有的段的信息:

readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x418:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000054  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000370
       0000000000000078  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000094
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  0000009c
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  0000009c
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a0
       0000000000000026  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000c6
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000c8
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  000003e8
       0000000000000030  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  00000120
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  00000188
       0000000000000180  0000000000000018          12    11     8
  [12] .strtab           STRTAB           0000000000000000  00000308
       0000000000000066  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

\

这样我们便可以看到,我们的文件一共有13个section header,这13个section header一起构成了section header table,section header table实际上就是section header的数组。我们同样可以在elf.h中查看section header的结构:

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;

在64位系统中,section header也是64个字节。
这里我们重新取了xxd的16进制结果进行比对,根据上一篇的结论,section header table从16进制的0x418开始:

0000410: 2100 0000 0000 0000 0000 0000 0000 0000  !...............
0000420: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000430: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000440: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000450: 0000 0000 0000 0000 2000 0000 0100 0000  ........ .......
0000460: 0600 0000 0000 0000 0000 0000 0000 0000  ................
0000470: 4000 0000 0000 0000 5400 0000 0000 0000  @.......T.......
0000480: 0000 0000 0000 0000 0100 0000 0000 0000  ................
0000490: 0000 0000 0000 0000 1b00 0000 0400 0000  ................
00004a0: 4000 0000 0000 0000 0000 0000 0000 0000  @...............
00004b0: 7003 0000 0000 0000 7800 0000 0000 0000  p.......x.......
00004c0: 0b00 0000 0100 0000 0800 0000 0000 0000  ................
00004d0: 1800 0000 0000 0000 2600 0000 0100 0000  ........&.......
00004e0: 0300 0000 0000 0000 0000 0000 0000 0000  ................
00004f0: 9400 0000 0000 0000 0800 0000 0000 0000  ................
0000500: 0000 0000 0000 0000 0400 0000 0000 0000  ................

第1个成员是段的名字,但在这里它并不是一个字符串而是一个32位的数,它的实际含义是是实际名字所存储的“section header string table”中的偏移,这个section header string table也是一个段,即前面看到的.shstrtab段。
第2个成员是段的类型。段的名字起到一定的标识作用,但真正的类型用这个字段表示。我们之前的readelf命令的结果中便包含类型,其中第0个段的类型是NULL,意味着这是一个无效的段,一般来讲section header table中的第0个段都是无效段,虽然有它的信息,但实际上并不存在这个段。NULL值对应的数值是0,从16进制文件中可以看到,它对应了64个字节的0值,同样可以看到我们的第1个段,代码段,它的值是1。事实上,代码段、数据段的类型均为1。
第3个成员是这个段的标志位,用于补充说明段的性质,如是否可写、是否需要分配实际空间以及是否可执行,其中最低位置1表示可写,第2位置1表示需要分配空间,第3位置1表示可执行,其它的位目前并没有被使用。从readelf命令的结果或者16进制文件中都可以看到,我们的第1个段的标志位值为6,表明需要分配空间,且可执行。第2个段和重定位有关,我们暂时跳过这个段,第3个段是我们熟悉的数据段,它的标志位是3,表明它需要分配空间,且可写(当然,全局变量的值是可以被改变的)。
第4个成员表明段被加载到内存中时的虚拟地址,我们这里是一个可重定位文件,所以这里的值为0,这个字段我们暂不深究。
第5个成员是这个段在文件中的偏移,正如和之前所看到的一致,我们的代码段从0x40处开始。
第6个成员是段的长度,不多说。
第7,8两个成员sh_link,sh_info和链接有关,这里我们也暂不关心。
第9个成员表示段地址的对齐要求,它的数值代表2的次幂,这主要用来限制第4个成员sh_addr的对齐,这里我们也暂不深究。
最后一个成员用来段中某些项的长度。有些段可能实际上是一种表(如前面提及的.shstrtab),里面排列了许多固定长度的项,这个字段就是对于那样的段表示项的长度。对于代码段之类的段来说这个字段没有意义。

这样我们对section header table也有了一个比较详细的了解,现在,我们可以具体地深入到段当中了。

string table

通常来讲一个ELF文件里会有两个字符串表,一个是.strtab,用来存储变量名称等,另一个则是之前有提到的.shstrtab。两者在结构上是基本一致的,我们先看.strtab段。
这个段从0x0308开始,长度为0x66,它的内容如下:

0000300:                     0053 696d 706c 6553  3........SimpleS
0000310: 6563 7469 6f6e 2e63 0073 7461 7469 635f  ection.c.static_
0000320: 7661 722e 3137 3532 0073 7461 7469 635f  var.1752.static_
0000330: 7661 7232 2e31 3735 3300 676c 6f62 616c  var2.1753.global
0000340: 5f69 6e69 745f 7661 7200 676c 6f62 616c  _init_var.global
0000350: 5f75 6e69 6e69 745f 7661 7200 6675 6e63  _uninit_var.func
0000360: 3100 7072 696e 7466 006d 6169            1.printf.main...

可以看到,.strtab以'\0'开始,每个字符串之间以'\0'分隔,这样,如果要获取一个字符串,只需给定首字符在这个段中的偏移就可以了。我们之前提到过.shstrtab,我们也看一下这个,它的偏移地址是0x120,长度是0x61:

0000120: 002e 7379 6d74 6162 002e 7374 7274 6162  ..symtab..strtab
0000130: 002e 7368 7374 7274 6162 002e 7265 6c61  ..shstrtab..rela
0000140: 2e74 6578 7400 2e64 6174 6100 2e62 7373  .text..data..bss
0000150: 002e 726f 6461 7461 002e 636f 6d6d 656e  ..rodata..commen
0000160: 7400 2e6e 6f74 652e 474e 552d 7374 6163  t..note.GNU-stac
0000170: 6b00 2e72 656c 612e 6568 5f66 7261 6d65  k..rela.eh_frame
0000180: 00

这里我们便看到了熟悉的段名,之前我们的代码段的偏移是0x20,正好就是.text。我们可以看到,rela.text和.text实际上共用一个串,不过初始的偏移不一样,我们之前讲过,这个段和重定位有关,rela.text和.text有一定的对应关系,这个我们稍后再提。
string table就是这样一种简单的字符串表,我们不再多提。

symbol table

symbol table(符号表)即我们的.symtab段,他记录了文件中函数、变量等的信息。我们可以使用nm命令查看一个目标文件的符号:

nm SimpleSection.o
0000000000000000 T func1
0000000000000000 D global_init_var
0000000000000004 C global_uninit_var
0000000000000021 T main
                 U printf
0000000000000004 d static_var.1752
0000000000000000 b static_var2.1753

我们看到我们定义的两个全局变量,我们的main函数,我们声明的printf函数,以及我们定义的两个静态局部变量,编译器对这两个变量进行了名称修饰,以避免重名。
类似地,我们先看看符号表的结构体:

typedef struct
{
  Elf64_Word	st_name;		/* Symbol name (string tbl index) */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char st_other;		/* Symbol visibility */
  Elf64_Section	st_shndx;		/* Section index */
  Elf64_Addr	st_value;		/* Symbol value */
  Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;

第一个字段是符号的名称,这里也就是在我们.strtab段里的索引。
第二个字段st_info包含了符号的类型信息和绑定信息,其中低4位用来表示类型,高4位用来表示绑定信息。关于这个字段各个数值的意义这里我们直接从elf.h里摘抄一下:

/* Legal values for ST_BIND subfield of st_info (symbol binding).  */

#define STB_LOCAL	0		/* Local symbol */
#define STB_GLOBAL	1		/* Global symbol */
#define STB_WEAK	2		/* Weak symbol */
#define	STB_NUM		3		/* Number of defined types.  */
#define STB_LOOS	10		/* Start of OS-specific */
#define STB_GNU_UNIQUE	10		/* Unique symbol.  */
#define STB_HIOS	12		/* End of OS-specific */
#define STB_LOPROC	13		/* Start of processor-specific */
#define STB_HIPROC	15		/* End of processor-specific */

/* Legal values for ST_TYPE subfield of st_info (symbol type).  */

#define STT_NOTYPE	0		/* Symbol type is unspecified */
#define STT_OBJECT	1		/* Symbol is a data object */
#define STT_FUNC	2		/* Symbol is a code object */
#define STT_SECTION	3		/* Symbol associated with a section */
#define STT_FILE	4		/* Symbol's name is file name */
#define STT_COMMON	5		/* Symbol is a common data object */
#define STT_TLS		6		/* Symbol is thread-local data object*/
#define	STT_NUM		7		/* Number of defined types.  */
#define STT_LOOS	10		/* Start of OS-specific */
#define STT_GNU_IFUNC	10		/* Symbol is indirect code object */
#define STT_HIOS	12		/* End of OS-specific */
#define STT_LOPROC	13		/* Start of processor-specific */
#define STT_HIPROC	15		/* End of processor-specific */

稍加说明一下,STB_LOCAL表示仅对目标文件可见的对象,包括局部变量、静态变量等STB_GLOBAL则是全局变量,STL_WEAK表示弱引用(关于弱引用相关的内容不在本文的讨论范围之内,如果有可能会另起一篇介绍),STT_OBJECT即表示变量、数组等,而STT_FUNC则是表示函数。
第3个字段目前总是为0,并没有被使用。
第4个字段表明了符号所在的段,对于确实定义在本目标文件中的符号,比如main,它的值是所在的段在section header table中的下标,对于printf这样并不定义在本目标文件的段,它的值是一些特殊值。
第5个字段表明了符号的值,通常来说就是变量、函数所在的地址。这里的地址仍然涉及到链接的一些概念,这里我们也暂时跳过。
第6个字段表明了符号的大小,比如double类型通常为8。

代码段

现在我们回过头来看一个比较简单的段,也就是我们熟悉的代码段,当然,代码段里有的就是我们的代码经过编译得到的机器指令。我们使用objdump -s -d命令来查看我们的代码段,其中-d选项用来将代码段的机器指令反汇编:

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 bf000000 00b80000 0000e800 000000c9  ................
 0020 c3554889 e54883ec 10c745f8 01000000  .UH..H....E.....
 0030 8b150000 00008b05 00000000 01c28b45  ...............E
 0040 f801c28b 45fc01d0 89c7e800 0000008b  ....E...........
 0050 45f8c9c3                             E...            
......
Disassembly of section .text:

0000000000000000 :
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	89 7d fc             	mov    %edi,-0x4(%rbp)
   b:	8b 45 fc             	mov    -0x4(%rbp),%eax
   e:	89 c6                	mov    %eax,%esi
  10:	bf 00 00 00 00       	mov    $0x0,%edi
  15:	b8 00 00 00 00       	mov    $0x0,%eax
  1a:	e8 00 00 00 00       	callq  1f 
  1f:	c9                   	leaveq 
  20:	c3                   	retq   

0000000000000021 
: 21: 55 push %rbp 22: 48 89 e5 mov %rsp,%rbp 25: 48 83 ec 10 sub $0x10,%rsp 29: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c 3c: 01 c2 add %eax,%edx 3e: 8b 45 f8 mov -0x8(%rbp),%eax 41: 01 c2 add %eax,%edx 43: 8b 45 fc mov -0x4(%rbp),%eax 46: 01 d0 add %edx,%eax 48: 89 c7 mov %eax,%edi 4a: e8 00 00 00 00 callq 4f 4f: 8b 45 f8 mov -0x8(%rbp),%eax 52: c9 leaveq 53: c3 retq

可以看到,代码段确确实实放着我们的代码,包括main函数和func1。这段汇编代码如何理解这里就暂时不作展开了。

数据段

最后我们再来看看我们的数据段。首先是我们的.data段,它用来存放被初始化的全局变量和静态局部变量,我们一共有全局变量global_init_var和静态变量static_var被初始化,所以.data段的大小是8。可以看到.data段中确实只储存了这两个变量:

Contents of section .data:
 0000 54000000 55000000                    T...U...

.rodata段用来存储只读数据,对应我们代码中printf使用的字符串常量"%d\n",我们的.rodata段只包含这个和一个'\0',共4字节:

Contents of section .rodata:
 0000 25640a00                             %d..

我们经常见到建议使用

const char *str = "some string";

而不用

const char str[] = "some string";

因为前者的字符串常量存储在程序的.rodata段,不会发生变化,而后者将在程序栈中占用空间,可能随着栈操作而频繁地被移动,这样浪费栈空间且可能降低效率。

.bss段用来存储为初始化的全局变量和局部静态变量。但是我们看到它的大小只有4字节,而且我们之前提到它实际上在目标文件中并不真实存在。因为这些变量并没有初值,所以在目标文件中并不需要进行存储,只需在实际将代码载入内存时分配即可;而通过symbol table我们可以知道,它实际上只保存了static_var2,并没有保存全局变量global_uninit_var,关于这部分的内容我们放到之后再讨论。

至此我们对段表以及一些类型的段进行了较为详细的介绍,但仍然有些段由于涉及到链接相关的内容而大量被我们跳过了,下一篇将重点讨论链接相关的问题,以把ELF目标文件和ELF可执行文件之间之间的区别阐明。是的,虽然有点不情愿,但是本来打算一篇讲完的东西分成了三篇,不知道会不会有第四篇,以及,无锁系列又要拖了,累觉不爱。