Lab_1:练习4——分析bootloader加载ELF格式的OS的过程
一、实验内容
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载ELF格式的OS?
二、实验相关
ELF文件格式
ELF(Executable and linking format)文件格式是Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
- 用于执行的可执行文件(executable file),用于提供程序的进程映像,加载到内存执行。 这也是本实验的OS文件类型。
- 用于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
- 共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
链接视图通过Section Header Table描述,执行视图通过Program Header Table描述。Section Header Table描述了所有Section的信息,包括所在的文件偏移和大小等;Program Header Table描述了所有Segment的信息,即Text Segment, Data Segment和BSS Segment,每个Segment中包含了一个或多个Section。
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)调用内核
2、为什么需要Bootloader?
每种不同的CPU体系结构都有不同的Bootloader。除了依赖于CPU的体系结构外,Bootloader还依赖于具体的嵌入式板级设备的配置,比如板卡的硬件地址分配,外设芯片类型等。也就是说,对于两块不同的开发板而言,即使他们是基于同一种CPU而构建的,但是如果他们的硬件资源或配置不一致的话,想要在一块开发板上运行Bootloader程序也能在另一块板子上运行,还是需要做修改。
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 = ;
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, ); // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> ) & 0xFF);
outb(0x1F5, (secno >> ) & 0xFF);
outb(0x1F6, ((secno >> ) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors // wait for disk to be ready
waitdisk(); // read a sector
insl(0x1F0, dst, SECTSIZE / );
} /* *
* 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) + ; // 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 * , ); // 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 ();
}
bootmain的内容:
bootasm.S完成了bootloader的大部分功能,包括打开A20,初始化GDT,进入保护模式,更新段寄存器的值,建立堆栈
接下来bootmain完成bootloader剩余的工作,就是把内核从硬盘加载到内存中来,并把控制权交给内核。
三、问题解答
问题一:bootloader如何读取硬盘扇区的?
读硬盘扇区的代码如下:
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), "" (addr), "" (cnt)
: "memory", "cc");
}
读取硬盘扇区的步骤:
等待硬盘空闲。waitdisk的函数实现只有一行:
while ((inb(0x1F7) & 0xC0) != 0x40)
,意思是不断查询读0x1F7寄存器的最高两位,直到最高位为0、次高位为1(这个状态应该意味着磁盘空闲)才返回。硬盘空闲后,发出读取扇区的命令。对应的命令字为0x20,放在0x1F7寄存器中;读取的扇区数为1,放在0x1F2寄存器中;读取的扇区起始编号共28位,分成4部分依次放在0x1F3~0x1F6寄存器中。
发出命令后,再次等待硬盘空闲。
硬盘再次空闲后,开始从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
- 从硬盘读了8个扇区数据到内存
0x10000
处,并把这里强制转换成elfhdr
使用; - 校验
e_magic
字段; - 根据偏移量分别把程序段的数据读取到内存中。
首先看readsect函数, readsect
从设备的第secno扇区读取数据到dst位置
static void
readsect(void *dst, uint32_t secno) {
waitdisk(); outb(0x1F2, ); // 设置读取扇区的数目为1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> ) & 0xFF);
outb(0x1F5, (secno >> ) & 0xFF);
outb(0x1F6, ((secno >> ) & 0xF) | 0xE0);
// 上面四条指令联合制定了扇区号
// 在这4个字节线联合构成的32位参数中
// 29-31位强制设为1
// 28位(=0)表示访问"Disk 0"
// 0-27位是28位的偏移量
outb(0x1F7, 0x20); // 0x20命令,读取扇区 waitdisk(); insl(0x1F0, dst, SECTSIZE / ); // 读取到dst位置,
// 幻数4因为这里以DW为单位
}
readseg简单包装了readsect,可以从设备读取任意长度的内容。
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count; va -= offset % SECTSIZE; uint32_t secno = (offset / SECTSIZE) + ;
// 加1因为0扇区被引导占用
// ELF文件从1扇区开始 for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
在bootmain函数中,
void
bootmain(void) {
// 首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * , ); // 通过储存在头部的幻数判断是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
} struct proghdr *ph, *eph; // 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);
}
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000 // 根据ELF头部储存的入口信息,找到内核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while ();
}
四、参考链接
Bootloader的作用、为什么需要Bootloader?
《ucore lab1 exercise4》实验报告
ucore_lab1
Lab_1:练习4——分析bootloader加载ELF格式的OS的过程的更多相关文章
- Lab1:练习四——分析bootloader加载ELF格式的OS的过程
练习四:分析bootloader加载ELF格式的OS的过程. 1.题目要求 通过阅读bootmain.c,了解bootloader如何加载ELF文件.通过分析源代码和通过qemu来运行并调试bootl ...
- 转:A10/A20 Bootloader加载过程分析
来自:http://blog.csdn.net/allen6268198/article/details/12905425 A10/A20 Bootloader加载过程分析 注:由于全志A10和A20 ...
- 【转】全志A10/A20 Bootloader加载过程分析
原文 : http://blog.csdn.net/allen6268198/article/details/12905425 从这里开始:http://linux-sunxi.org/Bootabl ...
- 【Spring源码分析】非懒加载的单例Bean初始化过程(下篇)
doCreateBean方法 上文[Spring源码分析]非懒加载的单例Bean初始化过程(上篇),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下 ...
- Spring源码分析:非懒加载的单例Bean初始化过程(下)
上文Spring源码分析:非懒加载的单例Bean初始化过程(上),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下AbstractAutowireC ...
- [zhuan]Dalvik 分析 - Class加载篇
http://blog.csdn.net/zhangyun438/article/details/17192787 内容如下: Java 源代码经过编译后会生成后缀为class的文件,也即字节码文件. ...
- 使用getJSON()方法异步加载JSON格式数据
使用getJSON()方法异步加载JSON格式数据 使用getJSON()方法可以通过Ajax异步请求的方式,获取服务器中的数组,并对获取的数据进行解析,显示在页面中,它的调用格式为: jQuery. ...
- hive 压缩全解读(hive表存储格式以及外部表直接加载压缩格式数据);HADOOP存储数据压缩方案对比(LZO,gz,ORC)
数据做压缩和解压缩会增加CPU的开销,但可以最大程度的减少文件所需的磁盘空间和网络I/O的开销,所以最好对那些I/O密集型的作业使用数据压缩,cpu密集型,使用压缩反而会降低性能. 而hive中间结果 ...
- cesium 加载shp格式的白模建筑
ceisum加载shp格式的建筑.有两种思路,目前推荐第二种. 方法一:将shp格式转换为geojson格式,然后采用cesium提供的接口加载到ceisum中. 严重缺陷:在面对大场景问题,即数据量 ...
随机推荐
- Redis基本使用(一)
redis window系统的redis是微软团队根据官方的linux版本高仿的 官方原版: https://redis.io/ 中文官网:http://www.redis.cn 1 redis下载和 ...
- 【开发笔记】- Grails框架定义一个不是数据库字段得属性
实体类 class Book{ String name String author // myfiled 我不想他在数据库中生成book表的字段 String myfield } 添加声明 class ...
- ES6-面向对象
1.老版的面向对象: function User(name,pass){ this.name=name; this.pass=pass; } User.prototype.showName=funct ...
- Docker 的操作命令记录
docker ps:列出正在运行的 container docker ps -a:列出所有的 container docker rm [containerid]:移除 container(可并列多个, ...
- javascript:void(0); 和 href = "#"
在做页面时,如果想做一个链接点击后不做任何事情,或者响应点击而完成其他事情,可以设置其属性 href = "#", 但是,这样会有一个问题,就是当页面有滚动条时,点击后会返回到页面 ...
- Centos7下安装redis并能使得外网访问
一.安装脚本 #!/bin/bash #FileName: install_redis_centos7.sh #Date: #Author: LiLe #Contact: @qq.com #Versi ...
- 剑指:包含min函数的栈(min栈)
题目描述 设计一个支持 push,pop,top 等操作并且可以在 O(1) 时间内检索出最小元素的堆栈. push(x)–将元素x插入栈中 pop()–移除栈顶元素 top()–得到栈顶元素 get ...
- Xpath re bs4 等爬虫解析器的性能比较
xpath re bs4 等爬虫解析器的性能比较 本文原始地址:https://sitoi.cn/posts/23470.html 思路 测试网站地址:http://baijiahao.baidu.c ...
- pycharm Launching unittests with arguments
在运行程序时出现 但是代码没有错 源代码是: 这是运行时启动了测试 解决方法: File-> Settings -> Tools -> Python Integrated Tools ...
- wordpress中文目录出现“有点尴尬诶!该页无法显示"
原因不详,可能是.htaccess.网上说删除后再更新固定链接会再生成,但是我没有.我又把原来的.htaccess上传后更改固定链接为“数字型”,测试后可以正常浏览. 然后又再更改为原来的“日期和名称 ...