工作关系,这个周花了一天时间好好研究了以下ELF文件及可执行ELF文件的加载。中间过程可谓收获不小,呵呵,因为之前搞linux驱动、ARM裸奔始终没有认真研究过ELF文件,这次深入学习一下,把之前很多没弄清的原理基本摸清楚了。
首先简单说明一下ELF文件的用途(呵呵,昨天讲解的时候直奔ELF的细节,结果被PL BS,说俺这样讲别人没做这块的根本不知道在讲什么,所以今天。。。),ELF文件一种UNIX文件格式,全称是executable and linkable format,即可执行链接格式,在UNIX系统及linux、BSD等类UNIX系统中广泛使用。这种文件格式主要作为链接目标文件用。ELF格式文件实际上有3类,可重定位文件、可执行文件、共享目标库文件。这三种文件用途不同,内部结构略有差别。可执行文件是由程序段(segment)构成,每个段由一个段描述项来描述该段的相关信息,比如该段在ELF文件内的偏移地址、该段的类型、该段在运行时的加载地址、该段的大小等等,所有段的描述项构成一个结构体数组,即程序段描述表,有些地方也叫程序头描述表,该表在ELF文件中的位置是在ELF文件头之后,而程序的各个段在ELF文件中则是紧接这程序段描述表之后。可重定位文件则是由节区(section)构成,与可执行文件的程序段类似,每个节区也有一个节区描述项,所有的节区描述项也构成一个数组。可重定位文件与可执行文件在结构上稍微有些区别,就是节区描述表是放在节区后面的,即在ELF文件内部是接着节区之后的,而不是像可执行文件中的程序段描述表那样放在程序段之前。另外可执行文件和可重定位文件的一个重要区别就是,可执行文件中,节区描述表是可选的,并非必须,实际情况中一般是不用节区描述表的;可重定位文件中,程序段描述表也是可选的,而非必须,同样实际情况中一般不用程序段描述表。至于这两种文件中对应的描述表如果不存在,则在ELF文件头中将对应成员设为0来表示。最后,共享目标库文件则是综合前面两种文件,段内分节区,段描述表和节区描述表都有!
介绍完ELF的大致结构之后,针对可执行ELF文件介绍一下该类文件中的几个比较重要的信息及相互之间的关系(下面所有介绍全部针对可执行文件)。ELF文件头可以用ELF32_Ehdr结构体来描述,该结构体具体信息不再罗嗦,网上随便找一下一大把介绍的,只介绍其中几个比较关键的成员。
首先是e_entry,这个成员描述的是可执行文件加载到内存中以后机器可执行的程序的入口地址!关于该程序还是罗嗦一下,因为她真的真的灰常灰常的重要!!!首先,e_enty的值是一个地址,指向内存中的一个单元;其次,e_entry指向的内存单元是存储CPU执行该ELF文件时执行的第一条指令的。简单说就是一个程序源代码中无论如何都会有一个唯一的入口,一般情况下,在有OS的情形下,对应C程序中的main,而在裸奔的情况下,一般都会用一个_start来指定程序的入口,这些标示符在源代码被链接成可执行文件并被加载到内存中之后,都会有一个明确的地址,CPU从该地址而且仅能从该地址开始执行这个程序才能正确执行,这个地址在链接成ELF文件时就被连接器写到ELF文件头中的e_entry成员中了,CPU正是通过e_entry才知道程序加载到内存之后该从什么地址(CPU是不认识程序符号的,比如main,_start之类的,CPU只知道地址)开始执行这个程序。
其次是e_poff,该成员标明了在ELF文件中,程序段描述表在ELF文件中的偏移。不罗嗦,因为必须要知道程序段描述表的位置,而通过e_phoff便可知道。
再次就是e_phentsize和e_phnum啦。对于ELF文件,每个程序段描述项的大小是一致的,因此之用一个成员来表示程序段描述项大小便可,不用每个描述项的大小都用一个成员来描述,e_phentsize正是用来描述描述项大小的。e_phnum则是描述程序段描述项个数的,即程序段描述表中表项的个数,重复一点,有几个描述项就表明ELF文件中有几个程序段。
接下来介绍上面说的程序段描述项。顾名思义,程序段描述项是描述程序段的,一点也不假!可执行程序中一般都会有.text,.data,.bss这些段,一般情况下是这样,但是不排除例外,比如还有.stack或者其他一些程序猿自定义段之类的,甚至有时仅仅只有一个.text段,不存在其他任何段也是可以滴。这些段在让CPU能执行前都需要先正确的加载到内存中去,这就需要相当多的信息才可以保证加载的正确性了,比如一个段要加载到内存的什么位置啊(p_vaddr或者p_paddr),加载程序从ELF的神马位置开始读取该段数据啊(p_poff),读取多少数据到内存中去啊(p_filesz和p_memsz)。
到这里已经差不多把ELF可执行文件的主要信息交代了,最后再罗嗦以下e_entry和入口所在段的p_vaddr(或者p_paddr)的关系吧。不知道是不是也会有人在开始有同我一样的疑惑,就是这俩成员的关系,能否替换使用!因为开始我一直YY可执行段开始存放的就是机器码,即可执行的第一条机器码就放在段的开始位置,于是俺就继续YY,那这个段加载到内存的地址p_vaddr就应该是和e_entry是一样的啦,而且无论何时都应该是一样的啦!于是乎还到处找资料,甚至上网所有相关信息来确认俺的YY想法!结果搜到一个仅仅简单介绍了一下p_vaddr <= e_entry < p_vaddr + p_filesz,当时看到这个真是蛋疼的紧!不明白那个<=中的<是何种情形下才成立!后来想了好久,冥冥之中才意识到segment的开头也是有可能会插播一下消息或者广告之类的东东,然后才接第一条机器码,如此以后e_entry便不会等于p_vaddr了,而是会稍微大一点点,至此方幡然醒悟!
阅读(607) | 评论(0) | 转发(0) |