转载请注明出处。https://rhirufxmbcyj.gitlab.io
上一篇中讲到了ELF文件的基本介绍、ELF中定义的数据类型以及ELF文件的ELF Header结构
接下来就按照官方给出的视图(链接视图和执行视图)来介绍program header
program header中存放的是系统加载可执行程序所需要的所有信息,包括解释器路径、需要映射到内存中的段、动态链接段等
多个program header组成了program header table,该表起始位置、每个表项的大小和表项的个数。(program header组成的表,表项当然是每一个program header)
所以,解析到ELF header后,就可以根据ELF header中的内容拿到program header table接着解析了
program header table的位置一般来说紧跟着ELF header后,但是也有可能被修改(暂且不提)
Program Header
program header的结构体描述:
从上图可以看出,32位的program header结构和64位的还是有一些区别的,判断的时候就需要注意,仅仅是一个字段的位置变了,由于每个字段占用的size可能是不同的,所以两者就会有较大的差异了
- p_type:用于描述该段的类型或解释该段的作用
- p_offset:该段(数据)在文件中的位置
- p_vaddr:该段在内存中的位置,也就是VA,如果是DYN类型的文件,这里就是RVA
- p_paddr:官方的解释是在物理地址关联的操作系统中,该字段是保留给物理地址的。由于 System V忽略了应用程序的物理地址,这个字段在可执行文件与共享文件中是未指定的。貌似意思就是不用管,但是看到基本上所有的gcc编译出的文件这个字段和p_vaddr的数据相同
- p_filesz:该段在文件中的大小
- p_memsz:该段在内存中的大小
- p_flags:段的标识,包括读、写、执行(PF_R、PF_W、PF_X)
- p_align:对齐粒度,且该值为0或1时表示不需要对齐。
这个结构体相对来说较为简单,字段也易懂,这里多介绍一下p_type
- PT_NULL:代表这个program header没有用到
- PT_LOAD:这个类型的program header 也就是常说的load segment,是需要加载到内存中的段,该段的file size不能比mem size大,而mem size多出file size的则初始化为0
- PT_DYNAMIC:代表这个段是存放动态链接信息的段
- PT_INTERP:该段存放的是解释器路径,是一个字符串。在程序执行期间,系统首先根据PT_INTERP拿到解释器的路径,然后创建解释器的进程映像,则程序启动时,控制权交给了解释器,解释器负责进行环境初始化工作(例如加载依赖的动态库),然后才会把控制权交给实际执行的程序进程。动态库被加载也需要解释器,并且解释器负责解决有可能出现的加载地址冲突的现象。
- PT_NOTE:辅助信息,没有关注过
- PT_SHLIB:保留
- PT_PHDR:program header table 本身
- PT_TLS:线程局部存储段,没有深入研究….
PC上常用的也就如上这些了,后边的一些定义貌似和特定处理器有关
Dynamic Program Header
Dynamic Program Header(Dynamic Segment)也是属于Program Header,为什么要单独拿出来说呢
因为这个段太特殊了,程序执行离不开他,简单来说,程序启动后,文件中的数据映射到内存,也就是PT_LOAD这个类型的段加载到了内存中去了,ELF header中有了入口点的地址,但是只有这些程序是不可能运行成功的
依赖的动态库还没有加载,重定位还没有处理,执行前的初始化代码还没有执行,如果是动态库,动态库是有导出符号的,但是连符号表在内存中哪个位置都不知道,也就无法导出 等等…..
所以这个就单独拿出来说一下
结构很简单,第一个字段是类型,第二个字段是联合体,说明这个字段有可能是个整型的value,有可能是个address。
类型有很多,很多我也没遇到过,挑些PC上常见的说
- DT_NULL:这个是用来当结束符的,当遇到DT_NULL类型的项以后,就不继续往下解析
- DT_NEEDED:依赖的动态库,值就是一个整型数值,是字符串表中的索引,根据这个索引就可以在字符串表对应的位置找到依赖动态库的名称
- DT_PLTRELSZ:PLT类型重定位的大小(PLT类型重定位是啥,以后介绍)
- DT_PLTGOT:.got.plt节的地址(.got.plt节是干啥的,以后介绍)
- DT_HASH:符号哈希表的地址
- DT_STRTAB:动态字符串表的地址,即.dynstr节在内存中的地址
- DT_SYMTAB:动态符号表的地址,.dynsym
- DT_RELA:RELA类型的重定位地址
- DT_RELASZ:RELA类型重定位总大小
- DT_RELAENT:RELA类型一个重定位的大小
- DT_STRSZ:动态字符串表的大小
- DT_SYMENT:动态符号表一个符号的大小
- DT_INIT:初始化函数的地址,先于main执行(参考此处了解linux编程初始化函数与终止函数)
- DT_FINI:终止函数的地址,main后执行
- DT_SONAME:动态库专属,指明该动态库的名字
- DT_RPATH:动态库专属,rpath,动态库搜索路径,linux程序员应该很清楚这个东西
- DT_REL:同上DT_RELA,不过是结构体有差异
- DT_RELSZ:REL类型重定位总大小
- DT_RELENT:一个REL类型重定位的大小
- DT_PLTREL:指明PLT重定位的类型,这一项的value就高级了,value的值是DT_REL(17)或DT_RELA(7)宏的值。
- DT_TEXTREL:说是重定位可能会改变.text节,没遇到过
- DT_JMPREL:PLT重定位的地址
- DT_GNU_HASH:GNU类型的hash表
- DT_VERSYM:.gnu.version节的地址
- DT_VERNEED:.gnu.version_r节的地址
- DT_VERNEEDNUM:”Number of needed versions“(暂时不了解)
- DT_INIT_ARRAY 、DT_FINI_ARRAY、DT_INIT_ARRAYSZ、DT_FINI_ARRAYSZ:elf.h中的解释是”Array with addresses of init function“,猜测可能是编写程序的时候可能会把多个函数设为初始化函数,那这多个函数就保存到这个array里。(不一定对,抽时间验证)
根据这些文件格式的介绍,可以接着上次的工程向下继续解析了。
经过验证,DT_INIT_ARRAY 、DT_FINI_ARRAY、DT_INIT_ARRAYSZ、DT_FINI_ARRAYSZ和上次猜想的一样。
验证方法如下:
linux下gcc编译代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __attribute__((constructor)) test1()
{
printf("hello test1\n");
}
void __attribute__((constructor)) test2()
{
printf("hello test2\n");
}
int main()
{
printf("hello main\n");
return 0;
}写了test1和test2两个初始化函数(没有在main中调用,但是先于main执行)
将编译好的文件a.out使用readelf查看Dynamic Program Header。
可以看到INIT_ARRAY的addr是0x600e00,换成文件偏移就是0xe00,大小是24个字节。
使用十六进制查看工具看0xe00是什么
程序是64位,这里边的24个字节正好是三个8字节,也就是三个地址,分别是0x400500,0x400526和0x400537
然后使用IDA看看这些分别是什么东西
挖一下第一个0x400500是什么鬼
可以看出后两个地址就是我们声明的初始化函数的地址,第一个是什么呢,名字那么长,怎么看怎么像系统函数,用readelf查看符号来看看
符号名少了个entry,估计是readelf的bug
看地址__frame_dummy_init_array_entry和__init_array_start的一样,看名字猜的话应该是init的起始函数
从stackoverflow上查到的答案,
这些标记该.init_array节的结束和开始,其中包含指向所有程序级初始值设定项的指针
,也就是为了方便系统找到我们写的初始化函数。