自制x86 Bootloader开发笔记(4)——— 编写ELF Loader

发布时间 2023-11-12 00:06:49作者: basic60

前言

我们的Bootloader目标是加载64位的ELF可执行文件,因此需要理解64位ELF文件的结构,并且支持运行ELF文件。

ELF文件结构

ELF文件的结构如下图所示:

图片名称

它包含了ELF头部,一个可选的Program Header Table,多个Section和一个Section Header Table。

其中ELF头部包含了整个ELF文件的一些信息。Section是ELF文件的一个一个的”节“,如代码段.code,数据段.data。Section Header Table则是记录了各个段的偏移和大小等信息的一样表。ELF_HEAD的结构如下:

struct elf_head {
    unsigned char e_ident[EI_NIDENT];
    uint16 e_type;
    uint16 e_machine;
    uint32 e_version;
    uint64 e_entry;
    uint64 e_phoff;
    uint64 e_shoff;
    uint32 e_flags;
    uint16 e_ehsize;
    uint16 e_phentsize;
    uint16 e_phnum;
    uint16 e_shentsize;
    uint16 e_shnum;
    uint16 e_shstrndx;
} ;

各个字段的具体含义可见引用1,我们只介绍其中最关键的e_entry,e_shoff,e_shnum字段。e_entry是程序的入口点,e_shoff是Section Header Table在文件中的偏移,e_shnum表示总共多少个Section Header,通过e_shoffe_shnum,我们就可以遍历Section Header Table中的各个表项,表项的结构如下:

struct elf_shdr {
    uint32 sh_name;
    uint32 sh_type;
    uint64 sh_flags;
    uint64 sh_addr;     // 节的虚拟地址
    uint64 sh_offset;   // 如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如sh_offset对于BSS 节来说是没有意义的
    uint64 sh_size;     // 节大小
    uint32 link;
    uint32 sh_info;
    uint64 sh_addralign;
    uint64 sh_entsize;
} ;

通过sh_offsetsh_addr以及sh_size这三个字段,我们就可以吧ELF文件的一个个节搬到指定的内存地址,然后跳转到程序入口点e_entry即可。代码如下

void load_elf_kernel(uint8* kaddr) {
    uint8* tmp = (uint8*) KERNEL_FILE_BASE;
    struct elf_head* ehead = (struct elf_head*) KERNEL_FILE_BASE;
    // 检测是否是elf文件
    if (ehead->e_ident[0] != 0x7f || ehead->e_ident[1] != 'E' || ehead->e_ident[2] != 'L' || ehead->e_ident[3] != 'F') {
        ld_print("Unsupported ABI! %d %d %d %d", ehead->e_ident[0], ehead->e_ident[1], ehead->e_ident[2], ehead->e_ident[3]);
        asm("hlt");     
    }

    struct elf_shdr* shdr_ptr = (struct elf_shdr*)(ehead->e_shoff + tmp);
    // 复制各个段到指定的内存地址
    for (int i = 0; i < ehead->e_shnum; i++) {
        uint8* target = (uint8*)shdr_ptr[i].sh_addr;
        uint8* src = tmp + shdr_ptr[i].sh_offset;
        for (int k = 0; k < shdr_ptr[i].sh_size; k++) {
            target[k] = src[k];
        }
    }
    // 跳转到elf文件入口
    jump_to_kernel(ehead->e_entry);
}

总结一下,整个流程如下:

  1. 读取ELF Header,获取程序入口点和Section Header Table位置。
  2. 读取Section Header Table。
  3. 遍历Section Header Table各个表项,将文件的各个节的内容复制到指定的内存地址。
  4. 跳转到程序入口点。

项目地址: https://github.com/basic60/ARCUS

引用

  1. https://uclibc.org/docs/elf-64-gen.pdf