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

1.题目要求

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

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

提示:可阅读“硬盘访问概述”,“ELF执行文件格式概述”这两小节。

2.整个流程

假定进入了保护模式之后,bootloader需要能够加载ELF文件。因为kenerl(就是ucore os)是以ELF的形式存在硬盘上的。

bootloader如何读取硬盘扇区的?就是说boot loader能够访问硬盘,bootloader把硬盘数据读取出来之后,要把其中ELF格式文件给分析出来。从而知道ucore它的代码段应该放在什么地方,应该有多大一块空间放这个代码段数据。哪一段空间是放数据段的数据,然后把它加载到内存中去,同时还知道跳转到ucore哪个位置去执行。

读取扇区是readsect函数,用到了in b,out b这种机器指令。in b,out b的实现都是内联汇编来实现的,它采取了一种IO空间的地址寻址方式,能够把外设的数据给读到内存中来,这也是x86里面的寻址方式。除了正常的memory方式之外,还有IO这一种寻址方式。

readsect函数这一块其实不用仔细去看,只需要知道bootloader从哪开始把相应的扇区给读进来,记忆它读多大,读完之后它就需要去进一步的分析。这个分析呢,需要去了解相应的ELF格式。

在bootmain函数中,有对ELF的格式判断,它怎么知道都进来这个扇区的数据是一个ELF格式的文件呢?它其实是读取了ELF的header,然后判断它的一个特殊的成员变量e_magic,看它是否等于一个特定的值,就认为确实是一个合法的ELF格式的文件。

在bootmain.c中有更详细的把ELF文件读取进来的一段判断。它怎么能够根据ELFheader和proghdr程序头来读出相应的代码段和数据段,然后加到相应的地方去。

最后一句

就是决定了bootloader把这个加载完之后,到底跳转到什么地方去,把控制权交给ucore去执行

3.预备知识

3.1ELF文件格式

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

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

ELF文件结构:

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

链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。可以看出,一个segment可以包含数个section。

本文关注执行,结构体Proghdr是用于描述段 (segment) 的 program header,可有多个。

ELF header在文件开始处描述了整个文件的组织。ELF的文件头包含整个执行文件的控制结构。

两个结构体都定义在elf.h中:

struct elfhdr {		 //ELF文件头
uint magic; // must equal ELF_MAGIC
uchar elf[12];
ushort type;
ushort machine;
uint version;
uint entry; // 程序入口的虚拟地址
uint phoff; // program header起始位置
uint shoff; //section header起始位置
uint flags;
ushort ehsize; // ELF文件头本身大小
ushort phentsize;
ushort phnum; // program header个数
ushort shentsize;
ushort shnum;
ushort shstrndx;
};
struct proghdr {//程序表头
uint type; // 段类型
uint offset; // 段相对于ELF文件开头的偏移
uint va; // 段的第一个字节将被放到内存中的虚拟地址
uint pa;// 物理地址
uint filesz;
uint memsz; // 段在内存映像中占用的字节数,就是在内存中的大小
uint flags; // 读,写,执行权限
uint align;
};

bootmain()函数的作用是加载 ELF格式的ucore操作系统,

3.2 bootmain()函数

#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()在这个文件中接管,读取内核并跳转到它
* */
// 扇区(sector)大小512
unsigned int SECTSIZE = 512 ;
// 将0x10000设为内核起始地址
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.
* */
//读取segment
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
// 从 0 开始读取 8*512 = 4096 byte 的内容到 ELFHDR
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0); // is this a valid ELF?
// 通过储存在头部的e_magic判断是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
} struct proghdr *ph, *eph; // load each program segment (ignores ph flags)
// 获得程序头表的起始位置 ph
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
// 获取程序头表结束的位置 eph
eph = ph + ELFHDR->e_phnum; // 按照描述表将ELF文件中数据载入内存
for (; ph < eph; ph ++) {
// 根据每个 program header 读取 segment
// 从 p_offset 开始拷贝 p_memsz 个 byte 到 p_pa
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);
}

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

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

现在看不懂这个函数具体怎么实现的没关系,后面会有具体的解释。只需要知道它的功能就行。

4. 问题解答

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

读硬盘扇区的代码如下:

/* 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(使用内联汇编实现),设置读取扇区的数目为1
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
// 上面四条指令联合制定了扇区号  
// 在这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.

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

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

之前已经看了readsect函数, readsect从设备的第secno扇区读取数据到dst位置

static void readsect(void *dst, uint32_t secno)

readseg简单包装了readsect,可以从设备读取任意长度的内容。

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;
// 加1因为0扇区被引导占用
// ELF文件从1扇区开始 for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}

最后是bootmain函数:

/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk
// 从 0 开始读取 8*512 = 4096 byte 的内容到 ELFHDR
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0); // is this a valid ELF?
// 通过储存在头部的e_magic判断是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
} struct proghdr *ph, *eph; // load each program segment (ignores ph flags)
// 获得程序头表的起始位置 ph
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
// 获取程序头表结束的位置 eph
eph = ph + ELFHDR->e_phnum; // 按照描述表将ELF文件中数据载入内存
for (; ph < eph; ph ++) {
// 根据每个 program header 读取 segment
// 从 p_offset 开始拷贝 p_memsz 个 byte 到 p_pa
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);
}

Lab1:练习四——分析bootloader加载ELF格式的OS的过程的更多相关文章

  1. Lab_1:练习4——分析bootloader加载ELF格式的OS的过程

    一.实验内容 通过阅读bootmain.c,了解bootloader如何加载ELF文件.通过分析源代码和通过qemu来运行并调试bootloader&OS, bootloader如何读取硬盘扇 ...

  2. (DT系列四)驱动加载中, 如何取得device tree中的属性

    本文以At91rm9200平台为例,从源码实现的角度来分析驱动加载时,Device tree的属性是如何取得的.一:系统级初始化DT_MACHINE_START 主要是定义"struct m ...

  3. 【转】(DT系列四)驱动加载中, 如何取得device tree中的属性

    原文网址:http://www.cnblogs.com/biglucky/p/4057488.html 本文以At91rm9200平台为例,从源码实现的角度来分析驱动加载时,Device tree的属 ...

  4. 转:A10/A20 Bootloader加载过程分析

    来自:http://blog.csdn.net/allen6268198/article/details/12905425 A10/A20 Bootloader加载过程分析 注:由于全志A10和A20 ...

  5. 【转】全志A10/A20 Bootloader加载过程分析

    原文 : http://blog.csdn.net/allen6268198/article/details/12905425 从这里开始:http://linux-sunxi.org/Bootabl ...

  6. Swift语法基础入门四(构造函数, 懒加载)

    Swift语法基础入门四(构造函数, 懒加载) 存储属性 具备存储功能, 和OC中普通属性一样 // Swfit要求我们在创建对象时必须给所有的属性初始化 // 如果没办法保证在构造方法中初始化属性, ...

  7. 【Spring源码分析】非懒加载的单例Bean初始化过程(下篇)

    doCreateBean方法 上文[Spring源码分析]非懒加载的单例Bean初始化过程(上篇),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下 ...

  8. Spring源码分析:非懒加载的单例Bean初始化过程(下)

    上文Spring源码分析:非懒加载的单例Bean初始化过程(上),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下AbstractAutowireC ...

  9. [zhuan]Dalvik 分析 - Class加载篇

    http://blog.csdn.net/zhangyun438/article/details/17192787 内容如下: Java 源代码经过编译后会生成后缀为class的文件,也即字节码文件. ...

随机推荐

  1. Linux重启网卡服务Failed to start LSB: Bring up/down networking.

    Linux网卡重启失败,使用 systemctl status network.service命令进行查看状态,发现启动有异常产生 network.service: control process e ...

  2. 使用 Spring 访问 Hibernate 的方法有哪些?

    我们可以通过两种方式使用 Spring 访问 Hibernate: 1. 使用 Hibernate 模板和回调进行控制反转 2. 扩展 HibernateDAOSupport 并应用 AOP 拦截器节 ...

  3. 如何在 Windows 和 Linux 上查找哪个线程使用的 CPU 时 间最长?

    使用 jstack 找出消耗 CPU 最多的线程代码

  4. 使用缓存(Cache)的几种方式,回顾一下~~~

    前言 如今缓存成为了优化网站性能的首要利器,缓存使用的好,不仅能让网站性能提升,让用户体验变好,而且还能节约成本(增加一台缓存服务器可能就节约好几台机器):那平时小伙伴们都使用哪些缓存方式呢?这里就来 ...

  5. 遇到MyBatis-Plus的错误之“Table 'mybatis_plus.user' doesn't exist”

    一.问题 Table 'mybatis_plus.user' doesn't exist 二.原因 表中没有user表 三.解决方案 生成user表既可 四.结果图 运行后显示查询出来的数据 五.总结 ...

  6. [开源] 分享自己用的 GitHub 分组管理工具.

    CODELF 的 GitHub Star 管理工具, 简洁快速,从开发者角度考虑,用完就走,不给开发者更多的管理负担. http://unbug.github.io/codelf/ 这个工具目前在 G ...

  7. 居中的css:完全指南(翻译)

    这里主要参考的是CHRIS COYIER写的一篇的文章(点击查看),主要讲了关于css水平.垂直居中的一些方法,每个方法后面都有一个demo,可以在线查看效果. 1 水平 水平居中有行内元素和块元素, ...

  8. 详解 CSS 属性 - position

    postion 属性定义了一个元素在页面布局中的位置以及对周围元素的影响.该属性共有5个值: position: absolute position: relative position: fixed ...

  9. H5本地存储:sessionStorage和localStorage

    作者:心叶时间:2018-05-01 18:30 H5提供了二种非常好用的本地存储方法:sessionStorage和localStorage,下面分别介绍一下: 1.sessionStorage:保 ...

  10. python-成绩转换

    本题要求编写程序将一个百分制成绩转换为五分制成绩.转换规则: 大于等于90分为A: 小于90且大于等于80为B: 小于80且大于等于70为C: 小于70且大于等于60为D: 小于60为E. 输入样例: ...