在编译之后,也就是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进行装载的吗?
这个在下一篇博文进行讲解。