课程中在讲到uboot debug时,需要加载img,
此时,如果没有SD卡等辅助启动设备,在Uboot中需要使用CCS加载ELF格式的img文件;
主要扩展一下这个ELF文件,其实是一个很重要的文件,
整理下ELF格式相关的数据,后面再做扩展,有兴趣的朋友可以一块讨论!!
打开一个ELF文件解读时,我们首先遇到的是一个ELF文件头。ELF文件头给出解读整个ELF文件的路径图,它是一个固定的结构。文件头的结构在系统头文件elf.h中定义,如果是32位的二进制文件,它是一个Elf32_Ehdr结构,如果是64位的二进制文件,则是一个Elf64_Ehdr结构。无论是何种结构,结构的第一个成员是一个16字节的e_ident,它给出了整个ELF文件的解读方式。究竟是32位的Elf32_Ehdr结构还是64位的Elf64_Ehdr结构,就看e_ident[4]的内容了。从文件偏移的角度来说,也就是文件偏移为4的字节确定了ELF文件究竟是32位的还是64位的。这里我们遵从习惯把文件开头的起始第一个字节的文件偏移约定为0,下面的所有叙述都遵从这个约定。
于是我们要做的第一件事是解读这个e_ident,确定ELF文件是32位的还是64位的,或者是其他位数的,从而确定ELF文件头的结构。为此,假定打开ELF文件时返回的文件描述符是fd,
lseek(fd,0,SEEK_SET);
read(fd,buf,16);
读出的buf里前四个字节是MagicNumber(对应文件偏移0-3)。如果
buf[0]=0x7f、buf[1]=''E''、buf[2]=''L''、buf[3]=''F''
则表明这是一个ELF格式的二进制文件,否则不是。如前面所述,我们首先关注的是buf[4]。如果buf[4]的值是1,则是32位的;如果是2,则是64位的。接下来是buf[5],它给出字节序特性。如果它的值是1,则是LSB的;如果是2,则是MSB的。对Intelx86机器,buf[5]=1;对SunSparc,buf[5]=2。跟着buf[5]的buf[6]给出ELF文件头的版本信息,当前它的值是EV_CURRENT(参见elf.h中的宏定义)。对buf[6]=EV_CURRENT的ELF文件头,从buf[7]开始,也即e_ident后面的9个字节全部为零,暂时没有使用。
现在确定了文件头的结构,我们就可以解读文件头了。下文中我们以32位的ELF文件为例来说明。对64位的,大同小异,把所有Elf32_***结构换成对应的Elf64_***结构,看看elf.h就什么都清楚了。32位的ELF文件头结构定义如下:
#defineEI_NIDENT(16)
typedefuint16_tElf32_Half;
typedefuint32_tElf32_Word;
typedefuint32_tElf32_Addr;
typedefuint32_tElf32_Off;
typedefstruct{
unsignedchare_ident[EI_NIDENT];/*上文所说的e_ident*/
Elf32_Halfe_type;/*文件类型*/
Elf32_Halfe_machine;/*机器类型*/
Elf32_Worde_version;/*文件版本*/
Elf32_Addre_entry;/*程序入口虚地址*/
Elf32_Offe_phoff;/*程序头表文件偏移*/
Elf32_Offe_shoff;/*节头表文件偏移*/
Elf32_Worde_flags;/*处理器相关的标志*/
Elf32_Halfe_ehsize;/*ELF文件头大小*/
Elf32_Halfe_phentsize;/*程序头表每个表项的大小*/
Elf32_Halfe_phnum;/*程序头表的表项数目*/
Elf32_Halfe_shentsize;/*节头表每个表项的大小*/
Elf32_Halfe_shnum;/*节头表的表项数目*/
Elf32_Halfe_shstrndx;/*节名字符串的节头表表项索引*/
}Elf32_Ehdr;
结构的各个成员的含义如注释中所解释的。对ELF文件,有两个视图,一个是从装载运行角度的,另一个是从连接角度的。从装载运行角度,我们关注的是程序头表,由程序头表的指引把ELF文件加载进内存运行它。从连接的角度,我们关注节头表,由节头表的指引把各个节连接组装起来。e_type的值与这两个视图相联系,由它我们可以知道能够从哪个视图去解读。如果e_type=1,表明它是重定位文件,可以从连接视图去解读它;如果e_type=2,表明它是可执行文件,至少可以从装载运行视图去解读它;如果e_type=3,表明它是共享动态库文件,同样可以至少从装载运行视图去解读它;如果e_type=4,表明它是Coredump文件,可以从哪个视图去解读依赖于具体的实现。
按照这两个视图,整个ELF文件的内容这样来组织:首先是ELF文件头,也就是上面的Elf32_Ehdr结构。或者对64位的ELF文件,是Elf64_Ehdr结构。ELF文件头位于文件开始处,无论e_type的值是什么,它是必须有的。其次是程序头表,对可执行文件(e_type=2)和动态库文件(e_type=3),它是必须有的。对重定位文件(e_type=1),程序头表的有无是可选的。例如用gcc的-c选项生成的.o文件,就没有程序头表。但无论如何,e_phoff和e_phnum、e_phentsize给出了ELF文件的程序头表信息。没有程序头表时它们的值为零。然后就是就是节头表,对可执行文件和动态库文件,它的有无是可选的,对重定位文件,它是必须有的。
e_shoff和e_shnum、e_shentsize给出节头表信息。最后就是文件的代码和数据这些具体内容了。如果有节头表,从连接视图去解读,ELF文件的具体代码和数据内容是以节为单位组织的。所有的代码和数据都分属于某一节,并且不能同时属于两个节。各个节不能交叉,不能有同时两个节覆盖同一内容。每一节在节头表中有一个表项与之对应,给出该节的相关信息。如果有程序头表,从装载运行视图去解读,所有代码和数据都分属于某一程序段。与连接视图不同,此时有交叉的情况。某些内容可能同时属于几个程序段,也即可能有几个段覆盖同一内容。同时,从程序头表来看,可能某些段不包含任何具体的代码和数据内容。例如,给出动态连接信息的程序段的所有内容都同时数据段。注意不要把这里所说的程序段与我们通常所说的文本段、数据段和堆栈段这几个概念相混淆,虽然它们有联系。程序加载进内存时,根据程序头表信息来就解读。
从连接视图来解读,其中有一节的内容是一些以零结尾的字符串。e_shstrndx给出该节在节头表中的表项索引。这些字符串是各节的名字。
了解了这些后,我们可以分别从两个视图来解读ELF文件了。先看连接视图,于是我们
Elf32_Ehdre_hdr;
void*SecHdrTbl;
lseek(fd,0,SEEK_SET);
read(fd,&e_hdr,sizeof(e_hdr));
SecHdrTbl=malloc(e_hdr.e_shnum*e_hdr.e_shentsize);
lseek(fd,e_hdr.e_shoff,SEEK_SET);
read(fd,SecHdrTbl,e_hdr.e_shnum*e_hdr.e_shentsize);
我们看看节头表是什么样的,因为节头表的各个表项给出了如何从连接视图解读ELF文件的路径图。节头表的每个表项是一个如下的结构:
typedefstruct
{
Elf32_Wordsh_name;/*节名索引*/
Elf32_Wordsh_type;/*节类型*/
Elf32_Wordsh_flags;/*加载和读写标志*/
Elf32_Addrsh_addr;/*执行时的虚地址*/
Elf32_Offsh_offset;/*在文件中的偏移*/
Elf32_Wordsh_size;/*字节大小*/
Elf32_Wordsh_link;/*与其他节的关联*/
Elf32_Wordsh_info;/*其他信息*/
Elf32_Wordsh_addralign;/*字节对齐*/
Elf32_Wordsh_entsize;/*如果由表项组成,每个表项的大小*/
}Elf32_Shdr;
再看装载运行视图:
void*ProHdrTbl;
ProHdrTbl=malloc(e_hdr.e_phnum*e_hdr.e_phentsize);
lseek(fd,e_hdr.e_phoff,SEEK_SET);
read(fd,SecHdrTbl,e_hdr.e_phnum*e_hdr.e_phentsize);
每个程序头表的每个表项的结构为:
typedefstruct
{
Elf32_Wordp_type;/*段类型*/
Elf32_Offp_offset;/*在文件中的偏移*/
Elf32_Addrp_vaddr;/*执行时的虚地址*/
Elf32_Addrp_paddr;/*执行时的物理地址*/
Elf32_Wordp_filesz;/*在文件中的字节数*/
Elf32_Wordp_memsz;/*在内存中的字节数*/
Elf32_Wordp_flags;/*标志*/
Elf32_Wordp_align;/*字节对齐*/
}Elf32_Phdr;
我们看一看这两个视图之间的相互关联,对动态库文件,共有三个程序段,如果是用gcc编译生成的,按文件偏移和虚地址增长次序排列,文本段包含如下这些节:
同样是按文件偏移和虚地址增长次序排列,数据段包含如下这些节:
另外还有一个程序段,它给出动态连接信息,它只包含有一节
我们看到,这一段与数据段有交叉了。此外还有一些节它们不属于任何一个程序段,这些节是:
对可执行文件,共有六个程序段,如果是用gcc编译生成的,按文件偏移和虚地址增长次序排列,文本段包含如下这些节:
同样是按文件偏移和虚地址增长次序排列,可执行文件的数据段包含如下这些节:
程序解释段(INTERP)与文本段相交叉,只包含.interp一节。给出动态连接信息的程序段同样与数据段相交叉,只包含
节。另一个程序段,与文本段相交叉,包含.note.ABI-tag节,它给出辅助信息。此外,还有一个程序段,它指程序头表自身。同动态库文件一样,下面的一些节不属于任何程序段: