Lab_1:练习四:分析bootloader加载ELF格式的OS的过程

1、实验内容

经过阅读bootmain.c,了解bootloader如何加载ELF文件。经过分析源代码和经过qemu来运行并调试bootloader&OS,html

  • bootloader如何读取硬盘扇区的?
  • bootloader是如何加载ELF格式的OS?

2、实验相关

ELF文件格式

ELF(Executable and linking format)文件格式是Linux系统下的一种经常使用目标文件(object file)格式,有三种主要类型:小程序

  • 用于执行的可执行文件(executable file),用于提供程序的进程映像,加载到内存执行。 这也是本实验的OS文件类型。
  • 用于链接的可重定位文件(relocatable file),可与其它目标文件一块儿建立可执行文件和共享目标文件。
  • 共享目标文件(shared object file),链接器可将它与其它可重定位文件和共享目标文件链接成其它的目标文件,动态链接器又可将它与可执行文件和其它共享目标文件结合起来建立一个进程映像。
ELF文件有两种视图(View),连接视图和执行视图,以下图:



连接视图经过Section Header Table描述,执行视图经过Program Header Table描述。Section Header Table描述了全部Section的信息,包括所在的文件偏移和大小等;Program Header Table描述了全部Segment的信息,即Text Segment, Data Segment和BSS Segment,每一个Segment中包含了一个或多个Section。

 

对于加载可执行文件,咱们只需关注执行视图,即解析ELF文件,遍历Program Header Table中的每一项,把每一个Program Header描述的Segment加载到对应的虚拟地址便可,而后从ELF header中取出Entry的地址,跳转过去就开始执行了。对于ELF格式的内核文件来讲,这个工做就须要由Bootloader完成。Bootloader支持ELF内核文件加载以后,用C语言编写的内核编译完成以后就不须要objcopy了。
 

Bootloader

咱们知道计算机启动是从BIOS开始,再由BIOS决定从哪一个设备启动以及启动顺序,好比先从DVD启动再从硬盘启动等。计算机启动后,BIOS根据配置找到启动设备,并读取这个设备的第0个扇区,把这个扇区的内容加载到0x7c00,以后让CPU从0x7c00开始执行,这时BIOS已经交出了计算机的控制权,由被加载的扇区程序接管计算机。
这第一个扇区的程序就叫Boot,它通常作一些准备工做,把操做系统内核加载进内存,并把控制权交给内核。因为Boot只能有一个扇区大小,即512字节,它所能作的工做颇有限,所以它有可能不直接加载内核,而是加载一个叫Loader的程序,再由Loader加载内核。由于Loader不是BIOS直接加载的,因此它能够突破512字节的程序大小限制(在实模式下理论上能够达到1M)。若是Boot没有加载Loader而直接加载内核,咱们能够把它叫作Bootloader。
Bootloader加载内核就要读取文件,在实模式下能够用BIOS的INT 13h中断。内核文件放在哪里,怎么查找读取,这里牵涉到文件系统,Bootloader要从硬盘(软盘)的文件系统中查找内核文件,所以Bootloader须要解析文件系统的能力。GRUB是一个专业的Bootloader,它对这些提供了很好的支持。
对于一个Toy操做系统来讲,能够简单处理,把内核文件放到Bootloader以后,即从软盘的第1个扇区开始,这样咱们能够不须要支持文件系统,直接读取扇区数据加载到内存便可。

一、Bootloader的做用

简单的说,BootLoader就是在操做系统运行以前运行的一段小程序。经过这段小程序,能够初始化硬件设备,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操做系统作好准备。对于Bootloader的启动过程又分为两个阶段stage1和stage2。函数

stage1所有由汇编编写,它的主要工做是(1)初始化硬件设备、(2)为加载Bootlodader的stage2准备RAM空间(3)拷贝Bootloader的stage2到RAM空间(4)设置好堆栈段为stager2的C语言环境作准备。布局

stage2所有由C语言编写,其的主要工做是(1)初始化本阶段要使用到的硬件设备(2)将内核映像和根文件系统映像从 flash 上读到RAM (3)调用内核

post

二、为何须要Bootloader?

每种不一样的CPU体系结构都有不一样的Bootloader。除了依赖于CPU的体系结构外,Bootloader还依赖于具体的嵌入式板级设备的配置,好比板卡的硬件地址分配,外设芯片类型等。也就是说,对于两块不一样的开发板而言,即便他们是基于同一种CPU而构建的,可是若是他们的硬件资源或配置不一致的话,想要在一块开发板上运行Bootloader程序也能在另外一块板子上运行,仍是须要作修改。

ui

bootmain.c代码

#include <defs.h> #include <x86.h> #include <elf.h>

/* ********************************************************************* * 这是一个很是简单的引导加载程序,它的惟一工做就是引导 * 来自第一个IDE硬盘的ELF内核映像 * * 磁盘布局 * 这个程序(bootasm)。S和bootmain.c)是引导加载程序。 * 应该存储在磁盘的第一个扇区。 * * *第二个扇区包含内核映像。 * * * 内核映像必须是ELF格式。 * * 开机步骤 * * 当CPU启动时,它将BIOS加载到内存中并执行它 * * * BIOS初始化设备,设置中断例程,以及 * 读取启动设备(硬盘)的第一个扇区 * 进入内存并跳转到它。 * * * Assuming this boot loader is stored in the first sector of the * hard-drive, this code takes over... * * * 控制启动bootasm.S -- 设置保护模式, * 和一个堆栈,C代码而后运行,而后调用bootmain() * * * bootmain()在这个文件中接管,读取内核并跳转到它 * */ unsigned int    SECTSIZE  =      512 ; struct elfhdr * ELFHDR    =      ((struct elfhdr *)0x10000) ;     // scratch space

/* waitdisk - wait for disk ready */
static void waitdisk(void) { while ((inb(0x1F7) & 0xC0) != 0x40) /* do nothing */; } /* 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
    outb(0x1F3, secno & 0xFF); outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF); outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors // wait for disk to be ready
 waitdisk(); // read a sector
    insl(0x1F0, dst, SECTSIZE / 4); } /* * * 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
    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.
    for (; va < end_va; va += SECTSIZE, secno ++) { readsect((void *)va, secno); } } /* 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?
  // 经过储存在头部的幻数判断是不是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph, *eph; // load each program segment (ignores ph flags)
   // ELF头部有描述ELF文件应加载到内存什么位置的描述表,
   // 先将描述表的头地址存在ph

ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); eph = ph + ELFHDR->e_phnum;

   // 按照描述表将ELF文件中数据载入内存
for (; ph < eph; ph ++) { readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); } // call the entry point from the ELF header // note: does not return
   // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
   // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
   // 根据ELF头部储存的入口信息,找到内核的入口

((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
  
//跳到内核程序入口地址,将cpu控制权交给ucore内核代码
bad: 
  outw(
0x8A00, 0x8A00);
  outw(
0x8A00, 0x8E00);

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

 

bootmain的内容:

bootasm.S完成了bootloader的大部分功能,包括打开A20,初始化GDT,进入保护模式,更新段寄存器的值,创建堆栈this

接下来bootmain完成bootloader剩余的工做,就是把内核从硬盘加载到内存中来,并把控制权交给内核。url

3、问题解答

问题一:bootloader如何读取硬盘扇区的?

读硬盘扇区的代码以下:spa

static voidreadsect(void *dst, uint32_t secno) {操作系统

 // wait for disk to be ready waitdisk(); //读取扇区内容 outb(0x1F2, 1); // count = 1 outb(使用内联汇编实现),设置读取扇区的数目为1 outb(0x1F3, secno & 0xFF); outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF); outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); outb(0x1F7, 0x20); // cmd 0x20 - read sectors
  // 上面四条指令联合制定了扇区号
  // 在这4个字节联合构成的32位参数中
  // 29-31位强制设为1
  // 28位(=0)表示访问"Disk 0"
  // 0-27位是28位的偏移量
// wait for disk to be ready waitdisk(); //将扇区内容加载到内存中虚拟地址dst // read a sector insl(0x1F0, dst, SECTSIZE / 4); //也用内联汇编实现 }

 就是把硬盘上的kernel,读取到内存中

outb()能够看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的(即全部的IO操做是经过CPU访问硬盘的IO地址寄存器完成)。从磁盘IO地址和对应功能表能够看出,该函数一次只读取一个扇区。  

IO地址 功能
0x1f0 读数据,当0x1f7不为忙状态时,能够读。
0x1f2 要读写的扇区数,每次读写前,你须要代表你要读写几个扇区。最小是1个扇区
0x1f3 若是是LBA模式,就是LBA参数的0-7位
0x1f4 若是是LBA模式,就是LBA参数的8-15位
0x1f5 若是是LBA模式,就是LBA参数的16-23位
0x1f6 第0~3位:若是是LBA模式就是24-27位 第4位:为0主盘;为1从盘
0x1f7 状态和命令寄存器。操做时先给命令,再读取,若是不是忙状态就从0x1f0端口读数据

其中insl的实现以下:

// x86.h
static inline void insl(uint32_t port, void *addr, int cnt) { asm volatile ( "cld;"
            "repne; insl;" : "=D" (addr), "=c" (cnt) : "d" (port), "0" (addr), "1" (cnt) : "memory", "cc"); }

读取硬盘扇区的步骤:

  1. 等待硬盘空闲。waitdisk的函数实现只有一行:while ((inb(0x1F7) & 0xC0) != 0x40),意思是不断查询读0x1F7寄存器的最高两位,直到最高位为0、次高位为1(这个状态应该意味着磁盘空闲)才返回。

  2. 硬盘空闲后,发出读取扇区的命令。对应的命令字为0x20,放在0x1F7寄存器中;读取的扇区数为1,放在0x1F2寄存器中;读取的扇区起始编号共28位,分红4部分依次放在0x1F3~0x1F6寄存器中。

  3. 发出命令后,再次等待硬盘空闲。

  4. 硬盘再次空闲后,开始从0x1F0寄存器中读数据。注意insl的做用是"That function will read cnt dwords from the input port specified by port into the supplied output array addr.",是以dword即4字节为单位的,所以这里SECTIZE须要除以4.

问题二:bootloader如何加载ELF格式的OS

  1. 从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用;
  2. 校验e_magic字段;
  3. 根据偏移量分别把程序段的数据读取到内存中。

4、参考连接

Bootloader的做用、为何须要Bootloader?

《ucore lab1 exercise4》实验报告

ucore_lab1

相关文章
相关标签/搜索