先来解释一下名词,ELF的英文全称是Executable and Linkable Format。可执行和可链接的文件。
和elf文件对应的是bin文件,bin文件是直接加载到内存中执行的文件,用uboot直接把bin文件拷贝到bin文件的运行地址,(注意,一定要拷贝到运行地址)这时使用go命令就能够执行刚才拷贝的bin文件。
elf文件需要用加载器进行加载,由于elf文件已经包含了程序的加载地址,因此可以把elf文件复制到内存中的非bin文件加载地址。(这里所说的bin文件是加载器解析elf完成以后的bin文件内容)。
了解elf文件的结构可以使用readelf命令 ,先用readelf -h 来看一下elf头信息。
jiefang@jiefang-virtual-machine:/home/yhl/worktest$ readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400670
Start of program headers: 64 (bytes into file)
Start of section headers: 7032 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28
jiefang@jiefang-virtual-machine:/home/yhl/worktest$
可以看到elf文件包含的头信息,这里注意一下几点信息,
第一是幻数的第2,3,4个,他们是'E' ,'L','F'三个字母,在解析的时候可以通过他们来判断文件的类型是否正确。
第二个是Entry point address,这个就是解析出来的bin文件需要存放的地址。
接下来我们通过C代码自己来解析一下elf文件的头信息,在/usr/include/目录下找到elf.h文件。
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
可以找到这个结构,注意,在结构体的上方有这么一句话,
/* The ELF file header. This appears at the start of every ELF file. */
这说明,我们可以把elf文件定位到0位置,然后用该结构体去对齐就好。下面是部分实现代码:
Elf32_Ehdr *elf_head; //elf 头文件 大小为52个字节
Elf32_Phdr *prg_head; //程序头
int fd = open("./bootrom",O_RDWR);
if(fd<0){
printf("open file error\n");
}
//开始解析elf头
elf_head = (Elf32_Ehdr *)malloc(sizeof(Elf32_Ehdr));
read(fd,elf_head,(sizeof(Elf32_Ehdr)));
if(elf_head->e_ident[0]==0x7f)
{
printf("this is a elf file\n");
}
else
{
printf("this isn't a elf file\n");
goto elf_head_err;
}
printf("p_shnum = %d\n",elf_head->e_phnum);
printf("p_shoff = %d\n",elf_head->e_phoff);
printf("e_phentsize = %d\n",elf_head->e_phentsize);
这里只解析了程序头的一些信息,为下面elf转bin文件做好铺垫。
这里先来看一张图,来说明elf文件的结构:
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。
我们的目的是C语言实现elf文件的加载,因此这里不关注左侧的。首先找到程序头的开始位置,在efl文件头中已经有,我们只需要在头文件中找到程序头对应的结构体,然后对应一下就可以了。
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
下面的实现代码来源于uboot源代码中的do_bootvx()函数:
/*
* A very simple ELF loader, assumes the image is valid, returns the
* entry point address.
*
* The loader firstly reads the EFI class to see if it's a 64-bit image.
* If yes, call the ELF64 loader. Otherwise continue with the ELF32 loader.
*/
static unsigned long load_elf_image_phdr(unsigned long addr)
{
Elf32_Ehdr *ehdr; /* Elf header structure pointer */
Elf32_Phdr *phdr; /* Program header structure pointer */
int i;
ehdr = (Elf32_Ehdr *)addr;
if (ehdr->e_ident[EI_CLASS] == ELFCLASS64)
return load_elf64_image_phdr(addr);
phdr = (Elf32_Phdr *)(addr + ehdr->e_phoff);
/* Load each program header */
for (i = 0; i < ehdr->e_phnum; ++i) {
void *dst = (void *)(uintptr_t)phdr->p_paddr;
void *src = (void *)addr + phdr->p_offset;
debug("Loading phdr %i to 0x%p (%i bytes)\n",
i, dst, phdr->p_filesz);
if (phdr->p_filesz)
memcpy(dst, src, phdr->p_filesz);
if (phdr->p_filesz != phdr->p_memsz)
memset(dst + phdr->p_filesz, 0x00,
phdr->p_memsz - phdr->p_filesz);
flush_cache((unsigned long)dst, phdr->p_filesz);
++phdr;
}
return ehdr->e_entry;
}
在for循环中,需要循环e_phnum次,这个参数只elf头中说明段的个数的结构成员。也就是说需要把每个段从自己的偏移地址拷贝到物理地址中去。两个if是在判断段中的内容,具体现在还说不清,以后补充,留下一个?。
以上内容就实现了一个elf文件加载器,但是只能在物理内存上面运行,虚拟内存不能给固定的地址写数据,以后在探索一下。
暂时留下两个问题
1、段中数据内容,.text .data .bbs 是如何在内存中分布的。链接脚本??
2、虚拟内存怎么给固定的地址上些数据??类似于裸机程序 int addr = 0x12345678; *(int *)addr=123;