操作系统19----bootloader加载OS源码分析

经过bootasm.S中汇编指令执行,处理器为操作系统内核加载准备一系列设置,设置寄存器值,开A20,初始化GDT,进入保护模式,设置段寄存器,建立堆栈,之后便执行从硬盘加载操作系统内核。

https://blog.csdn.net/u014106644/article/details/96965473

首先操作系统内核代码被编译链接称为ELF格式的可执行文件,然后由bootloader加载到内存中,并且跳转到内核程序入口。

OS ELF格式文件

有关ELF文件格式分析可以参考

目标文件和ELF格式详解 https://www.jianshu.com/p/132412ca73ce

ELF文件解析和加载(附代码)  https://blog.csdn.net/muaxi8/article/details/79627859

ELF文件格式解析 https://blog.csdn.net/dddxxxx/article/details/80347610

Linux生成的目标文件是标准的ELF文件格式,其基本结构如下所示

è¿éåå¾çæè¿°

ELF文件格式提供了两种视图,分别是链接视图和执行视图。

链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。总个文件可以分为四个部分:

- ELF header: 描述整个文件的组织。
- Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。
- sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。从图中我们也可以看出,segments与sections是包含的关系,一个segment包含若干个section。
- Section Header Table: 包含了文件各个segction的属性信息。

对应文件头数据结构如下

/* file header */
struct elfhdr {
    uint32_t e_magic;     // must equal ELF_MAGIC
    uint8_t e_elf[12];
    uint16_t e_type;      // 1=relocatable, 2=executable, 3=shared object, 4=core image
    uint16_t e_machine;   // 3=x86, 4=68K, etc.
    uint32_t e_version;   // file version, always 1
    uint32_t e_entry;     // entry point if executable
    uint32_t e_phoff;     // file position of program header or 0
    uint32_t e_shoff;     // file position of section header or 0
    uint32_t e_flags;     // architecture-specific flags, usually 0
    uint16_t e_ehsize;    // size of this elf header
    uint16_t e_phentsize; // size of an entry in program header
    uint16_t e_phnum;     // number of entries in program header or 0
    uint16_t e_shentsize; // size of an entry in section header
    uint16_t e_shnum;     // number of entries in section header or 0
    uint16_t e_shstrndx;  // section number that contains section name strings
};

e_entry表示可执行程序的入口地址,即系统加载该ELF后,应该从该地址开始运行;

e_phoff程序头的地址

e_phnum程序头的数量

在ELF中把权限相同、又连在一起的段(section)叫做segment,操作系统正是按照“segment”来映射可执行文件的。

描述这些“segment”的结构叫做程序头,它描述了elf文件该如何被操作系统映射到内存空间中。

程序头数据结构如下

/* program section header */
struct proghdr {
    uint32_t p_type;   // loadable code or data, dynamic linking info,etc.
    uint32_t p_offset; // file offset of segment
    uint32_t p_va;     // virtual address to map segment
    uint32_t p_pa;     // physical address, not used
    uint32_t p_filesz; // size of segment in file
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    uint32_t p_flags;  // read/write/execute bits
    uint32_t p_align;  // required alignment, invariably hardware page size
};

p_va该段映射到内存中的段虚拟地址;

p_memsz表示该段在内存中的大小;

p_offset该段在可执行文件中的偏移;


bootloader加载ELF格式OS过程

核心代码如下:

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk  首先读取ELF的头部
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    // ELF头部有描述ELF文件应加载到内存什么位置的描述表,
    // 先将描述表的头地址存在ph
    //uint32_t e_phoff;  // file position of program header or 0
    //uint16_t e_phnum;  // number of entries in program header or 0
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;

    // 按照描述表将ELF文件中数据载入内存
    //uint32_t p_va;     // virtual address to map segment
    //uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    //uint32_t p_offset; // file offset of segment
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
    // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000

    // call the entry point from the ELF header
    // note: does not return
    // 根据ELF头部储存的入口信息,找到内核的入口
    //uint32_t e_entry;     // entry point if executable
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

磁盘第一个扇区存储bootloader代码,从第二个扇区往后存储操作系统内核镜像,镜像使用ELF格式来存储。

readsect函数

从设备的第secno扇区读取数据到dst位置

/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
    // wait for disk to be ready
    waitdisk();

    outb(0x1F2, 1);                         // count = 1 设置读取扇区的数目为1
    outb(0x1F3, secno & 0xFF);
    outb(0x1F4, (secno >> 8) & 0xFF);
    outb(0x1F5, (secno >> 16) & 0xFF);
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);  // 上面四条指令联合制定了扇区号
												// 在这4个字节线联合构成的32位参数中
												//   29-31位强制设为1
												//   28位(=0)表示访问"Disk 0"
												//   0-27位是28位的偏移量
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors  0x20命令,读取扇区

    // wait for disk to be ready
    waitdisk();

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4);  //读取到dst位置,幻数4因为这里以DW为单位
}

readseg函数

readseg(uintptr_t va, uint32_t count, uint32_t offset)

从内核镜像offset偏移位置处读取count字节到虚拟地址va位置处。

/* *
 * readseg - read @count bytes at @offset from kernel into virtual address @va,
 * might copy more than asked.
 * */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
	//读取完数据之后,地址指针位置
    uintptr_t end_va = va + count;

    // round down to sector boundary
    va -= offset % SECTSIZE;

    // translate from bytes to sectors; kernel starts at sector 1
     加1因为0扇区被引导占用  ELF文件从1扇区开始 计算offset位于磁盘扇区号
    uint32_t secno = (offset / SECTSIZE) + 1;

    // If this is too slow, we could read lots of sectors at a time.
    // We'd write more to memory than asked, but it doesn't matter --
    // we load in increasing order.
    //每次读取一个扇区内容,直到大于或者等于count为止
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}

对于bootmain主函数,首先读取ELF的头部

readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

检测ELF文件魔术是否正确

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

然后找到程序头描述表的地址以及程序头数量

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    // ELF头部有描述ELF文件应加载到内存什么位置的描述表,
    // 先将描述表的头地址存在ph
    //uint32_t e_phoff;  // file position of program header or 0
    //uint16_t e_phnum;  // number of entries in program header or 0
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;

根据描述符表,将每个段从磁盘加载到内存相应位置

    // 按照描述表将ELF文件中数据载入内存
    //uint32_t p_va;     // virtual address to map segment
    //uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    //uint32_t p_offset; // file offset of segment
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

跳转到程序入口地址处

    // 根据ELF头部储存的入口信息,找到内核的入口
    //uint32_t e_entry;     // entry point if executable
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

至此已经完成,内核从硬盘加载到内存,操作系统正式启动。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值