静态程序编译链接与装载(二)编译链接以及ELF中section详细分析

在编译之后,也就是make compile,将a.c文件,转换成a.s。

a.c和a.s如下图所示:


程序被切分成了如下的一些段:

  • 1个代码段, .text

    • 函数main

  • 2个数据段,.data

    • 全局变量global_init_a

    • 静态局部变量static_a

  • 2个COM段,.comm

    • 变量global_noinit_b

    • 变量static_b

得到.s之后,就可以使用as汇编器,将.s代码,汇编成 .o 文件

.o文件是二进制文件,ELF文件格式,使用gvim直接打开,可以看到二进制数据。但是不易读。


使用readelf命令,查看该elf的详细信息。

之后,就开始要介绍下,大名鼎鼎的ELF文件。

elf文件,是linux下,定义的一种标准格式,包括以下四种类型:

  • 可重定义文件(relocatable file):这类文件包很了代码和数据,可以被链接成可执行程序,或者共享目标文件,比如.o文件

  • 可执行文件(executable file):这类文件包含了可以直接执行的程序

  • 共享目标文件(shared object file): 这种文件包含了代码和数据,库文件,比如.so

  • 核心转储文件(core dump file):当进程意外终止,系统可以将该进行的信息转存到该文件中,如linux下的core dump

    elf文件的组织格式如下图所示:


包括了:

  • ELF header:elf的头信息

  • sections:段信息

  • section header table:段表头信息

  • table:表信息

对于可执行程序,还有program header table信息。

一、a.o的ELF分析

下面来分析生成的a.o的ELF信息。

1、ELF头

elf头的结构体,在/usr/include/elf.h文件中,分32位版本和64位版本。我在结构体成员的注释中,将成员的偏移给加上去了,方便后面分析。


a.o文件的二进制文件内容如下:


将信息,进行解析,得到如下:

 成员

size

value

e_type

2

0x0001

e_machine

2

0x00b7

e_version

2

0x0001

e_entry

2

0x0000

e_phoff

4

0x0

e_shoff

4

0x2e8

e_flags

2

0x0000

e_ehsize

2

0x0040

e_phentsize

2

0x0000

e_phnum

2

0x0000

e_shentsize

2

0x0040

e_shnum

2

0x0009

e_shstrndx

2

0x0008

从上表可以知道:

  • ELF文件的入口执行地址是0

  • ELF文件的section表的起始位置,在0x2e8(对应十进制744)

  • ELF文件中有9个section,每个section header,占64个字节

  • ELF文件的第8个section,是.shstrtab段

使用readelf –h查看a.o,得到一致的信息:


从ELF的头信息,获取到了段表的信息,因此下一步,就是要分析section段。

2、section段表

section header的数据结构如下所示:


在这个结构体中,我们关心如下的一些成员:

  • sh_name:section的名字,.shstrtab段中的偏移

  • sh_type:section的类型

  • sh_addr:section的VMA地址

  • sh_offsset: section在ELF文件的起始地址

  • sh_size: section在ELF文件中的大小

从elf的起始地址0x000002e8开始,以64个字节进行切分,共切分9份。得到每个段的描述。这里就不从二进制文件来分析。

通过 readelf –S,可以查看段表信息


总共是9个段。

3、.shstrtab段

首先来分析.shstrtab段,因为这个段,保存了各个section的名字。

段表首地址是  0x000002e8, 每个段占用64个字节,也就是0x40,而.shstrtab段是第8个段

因此.shstrtab段表的首地址是  0x2e8 + 0x40*8 = 0 x4e8

二进制数据如下:


  

address

size

value

sh_name

0x4e8

4

0x11

sh_type

0x4ec

4

0x3

sh_flags

0x4f0

8

0x0

sh_addr

0x4f8

8

0x0

sh_offset

0x500

8

0x2a8

sh_size

0x508

8

0x3a

sh_link

0x510

4

0x0

sh_info

0x514

4

0x0

sh_addralign

0x518

8

0x01

sh_entsize

0x520

8

0x0

解析完,可以得到

  • shstrtab段的内容,从文件的 0x2a8开始

  • shstrtab段的大小,为 0x3a

  • shstrtab段的名字,从shstrtab段内容的偏移0x11处开始

下图是elf中,从0x2a0开始的内容:


从0x2a8开始,保存的就是各个section名字,每个字符串,以0作为结束。.shstrtab段的名字,偏移是0x11,  那么起始地址就是0x2a8 + 0x11 = 0x2b9,   刚好就是 .shstrtab。

4、.strtab表

.strtab表,是字符串表,保存了程序中的字符串,以及符号,如变量名,函数名,文件名等。解析方法,和.shstrtab是一致的,这里就不再分析了。

正是有这个表,反汇编信息中,才会看到函数名,变量名。

5、.text段

.text段,就是大名鼎鼎的代码段,默认情况下,代码都是放到这个段中的。

段表首地址是  0x000002e8, 每个段占用64个字节,也就是0x40,而.text段是第1个段。因此text段表的首地址是  0x2e8 + 0x40 = 0x328


映射到结构体:

  

address

size

value

sh_name

0x328

4

0x20

sh_type

0x32c

4

0x1

sh_flags

0x330

8

0x6

sh_addr

0x338

8

0x0

sh_offset

0x340

8

0x40

sh_size

0x348

8

0x2c

sh_link

0x350

4

0x0

sh_info

0x354

4

0x0

sh_addralign

0x358

8

0x4

sh_entsize

0x360

8

0x0

解析完,可以得到

  • .text段的内容,从文件的 0x40开始

  • .text段的type为1, 表示 程序段,也就是 PROGBITS

  • .text段的flags为6,表示可读可执行

  • .text段的大小,为 0x2c

  • .text段的VMA,为0x0

  • .text段的名字,从shstrtab段内容的偏移0x20处开始

查看ELF,0x40开始的数据


和反汇编中的数据一致

6、.symtab 符号表

符号表,保存了程序中的所有符号。

段表首地址是  0x000002e8, 每个段占用64个字节,也就是0x40,而.rela.text段是第6个段,因此.symtab段表的首地址是  0x2e8 + 0x40  * 6 = 0x468


符号表的结构体,如下所示:


解析,得到sh_offset为0xa8   sh_size为0x168

每个结构体,占24个字节。 大小为0x168,那么就有 0x168/0x18 = 16, 总共16个符号,这也是 readelf -s的结果。

可以看到 main和swap在这个符号表中。 静态变量,也在。

swap符号,是UNDEFINE,表示这个符号是不在本文件内。

7、.rela.text可重定位表

段表首地址是  0x000002e8, 每个段占用64个字节,也就是0x40,而.rela.text段是第2个段。因此rela.text段表的首地址是  0x2e8 + 0x40  * 2 = 0x368


通过解析,得到sh_offset为0x260, sh_size为0x48。

可重定位表结构体如下图所示:


结构体大小为 24个字节,   0x48/24 = 3, 表示有3个可重定位项,和readelf查看的结果一致。


.data + 4是静态变量 static_a。swap是外部函数。


以下是反汇编,对于访问静态静态和调用外部函数,还没有确定的地址。


从反汇编中,可以看出,

  • 偏移0x14: 对应 c代码中的  static_a =3 赋值

  • 偏移0x18:   对应代码中的 &static_a

  • 偏移0x1c:  对应代码中的  swap

在链接之后,可重定位的符号(包括变量和函数)的地址,就会修正到真正的地址。

下图是a.o和最终生成的out.elf之间的对应关系。


在a.o中,对swap的调用是  bl 0 <swap>。

在out.elf中,已经修正为 bl 0x40002c <swap>,因为swap函数的地址是0x4002c。

b.o以及out.elf的分析,与a.o的分析方法一致。这里就不再次的说明。out.elf的生成,依赖于连接器,以及链接脚本,将a.o以及b.o中的内容进行有效的组合。

以上分析的是,都是链接视图。

在执行的时候,elf中的指令和数据,是要装载到内存中,然后被执行的。但是在装载的过程中,是按照链接中的section进行装载的吗?

这个在下一篇博文进行讲解。

此条目发表在其他分类目录。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。