《ucore lab1 exercise4》实验报告
资源
题目:分析bootloader加载ELF格式的OS的过程
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,理解:
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载ELF格式的OS?
解答
问题1:bootloader如何读取硬盘扇区
分析原理
阅读材料其实已经给出了读一个扇区的大致流程:
- 等待磁盘准备好
- 发出读取扇区的命令
- 等待磁盘准备好
- 把磁盘扇区数据读到指定内存
实际操作中,需要知道怎样与硬盘交互。阅读材料中同样给出了答案:所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。硬盘共有8个IO地址寄存器,其中第1个存储数据,第8个存储状态和命令,第3个存储要读写的扇区数,第4~7个存储要读写的起始扇区的编号(共28位)。了解这些信息,就不难编程实现啦。
分析代码
bootloader读取扇区的功能是在boot/bootmain.c的readsect函数中实现的,先贴代码:
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);
}
根据代码可以得出读取硬盘扇区的步骤:
等待硬盘空闲。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.
问题2: bootloader如何加载ELF格式的OS
分析原理
首先从原理上分析加载流程。
bootloader要加载的是bin/kernel文件,这是一个ELF文件。其开头是ELF header,ELF Header里面含有phoff字段,用于记录program header表在文件中的偏移,由该字段可以找到程序头表的起始地址。程序头表是一个结构体数组,其元素数目记录在ELF Header的phnum字段中。
程序头表的每个成员分别记录一个Segment的信息,包括以下加载需要用到的信息:
- uint offset; // 段相对文件头的偏移值,由此可知怎么从文件中找到该Segment
- uint va; // 段的第一个字节将被放到内存中的虚拟地址,由此可知要将该Segment加载到内存中哪个位置
- uint memsz; // 段在内存映像中占用的字节数,由此可知要加载多少内容
根据ELF Header和Program Header表的信息,我们便可以将ELF文件中的所有Segment逐个加载到内存中
分析代码
bootloader加载os的功能是在bootmain函数中实现的,先贴代码:
void bootmain(void) {
// read the 1st page off disk
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)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
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
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
}
首先从硬盘中将bin/kernel文件的第一页内容加载到内存地址为0x10000的位置,目的是读取kernel文件的ELF Header信息。
校验ELF Header的e_magic字段,以确保这是一个ELF文件
读取ELF Header的e_phoff字段,得到Program Header表的起始地址;读取ELF Header的e_phnum字段,得到Program Header表的元素数目。
遍历Program Header表中的每个元素,得到每个Segment在文件中的偏移、要加载到内存中的位置(虚拟地址)及Segment的长度等信息,并通过磁盘I/O进行加载
加载完毕,通过ELF Header的e_entry得到内核的入口地址,并跳转到该地址开始执行内核代码
调试代码
输入
make debug启动gdb,并在bootmain函数入口处即0x7d0d设置断点,输入c跳到该入口单步执行几次,运行到call readseg处,由于该函数会反复读取硬盘,为节省时间,可在下一条语句设置断点,避免进入到readseg函数内部反复执行循环语句。(或者直接输入n即可,不用这么麻烦)
执行完readseg后,可以通过
x/xw 0x10000查询ELF Header的e_magic的值,查询结果如下,确实与0x464c457f相等,所以校验成功。注意,我们的硬件是小端字节序(这从asm文件的汇编语句和二进制代码的对比中不难发现),因此0x464c45实际上对应字符串"elf",最低位的0x7f字符对应DEL。
(gdb) x/xw 0x10000
0x10000: 0x464c457f
- 继续单步执行,由
0x7d2f mov 0x1001c,%eax可知ELF Header的e_phoff字段将加载到eax寄存器,0x1001c相对0x10000的偏移为0x1c,即相差28个字节,这与ELF Header的定义相吻合。执行完0x7d2f处的指令后,可以看到eax的值变为0x34,说明program Header表在文件中的偏移为0x34,则它在内存中的位置为0x10000 + 0x34 = 0x10034.查询0x10034往后8个字节的内容如下所示:
(gdb) x/8xw 0x10034
0x10034: 0x00000001 0x00001000 0x00100000 0x00100000
0x10044: 0x0000dac4 0x0000dac4 0x00000005 0x00001000
可以结合代码中定义的Program Header结构来理解这8个字节的含义。
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
};
还可以使用readelf -l bin/kernel来查询kernel文件各个Segment的基本信息,以作对比。查询结果如下所示,可见与gdb调试结果是一致的。
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x00100000 0x00100000 0x0dac4 0x0dac4 R E 0x1000
LOAD 0x00f000 0x0010e000 0x0010e000 0x00aac 0x01dc0 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
继续单步执行,由
0x7d34 movzwl 0x1002c,%esi可知ELF Header的e_phnum字段将加载到esi寄存器,执行完x07d34处的指令后,可以看到esi的值变为3,这说明一共有3个segment。后面是通过磁盘I/O完成三个Segment的加载,不再赘述。
《ucore lab1 exercise4》实验报告的更多相关文章
- [操作系统实验lab3]实验报告
[感受] 这次操作系统实验感觉还是比较难的,除了因为助教老师笔误引发的2个错误外,还有一些关键性的理解的地方感觉还没有很到位,这些天一直在不断地消化.理解Lab3里的内容,到现在感觉比Lab2里面所蕴 ...
- Ucore lab1实验报告
练习一 Makefile 1.1 OS镜像文件ucore.img 是如何一步步生成的? + cc kern/init/init.c + cc kern/libs/readline.c + cc ker ...
- ucore操作系统学习(三) ucore lab3虚拟内存管理分析
1. ucore lab3介绍 虚拟内存介绍 在目前的硬件体系结构中,程序要想在计算机中运行,必须先加载至物理主存中.在支持多道程序运行的系统上,我们想要让包括操作系统内核在内的各种程序能并发的执行, ...
- 《ucore lab3》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1:给未被映射的地址映射上物理页 题目 完成do_pgfault(mm/vmm.c)函数,给未被映射的地址映射上物理页.设置访问权限的时候需 ...
- 《ucore lab1 exercise5》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 题目:实现函数调用堆栈跟踪函数 我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_s ...
- 《ucore lab8》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 完成读文件操作的实现(需要编码) 题目 首先了解打开文件的处理流程,然后参考本实验后续的文件读写操作的过程分析,编写在sfs_inod ...
- 《ucore lab7》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码) 题目 完成练习0后,建议大家比较一下(可用meld等文件dif ...
- 《ucore lab6》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 使用 Round Robin 调度算法(不需要编码) 题目 完成练习0后,建议大家比较一下(可用kdiff3等文件比较软件) 个人完成 ...
- 《ucore lab5》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 加载应用程序并执行(需要编码) 题目 do_execv函数调用load_icode(位于kern/process/proc.c中) 来 ...
- 《ucore lab4》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1:分配并初始化一个进程控制块 题目 alloc_proc函数(位于kern/process/proc.c中) 负责分配并返回一个新的str ...
随机推荐
- es6 函数解构的用途
es6的变量解构赋值很方便,那具体有哪些用途呢? 1.变换变量的值 let n = 10; let m = 20; [n, m] = [m, n] 这样n , m 的值 会互换, 即:n = 20, ...
- The Architectural Principles Behind Vrbo’s GraphQL Implementation
转自:https://medium.com/expedia-group-tech/graphql-component-architecture-principles-homeaway-ede8a58d ...
- ESA2GJK1DH1K微信小程序篇: 测试微信小程序扫描Air202上面的二维码绑定设备,并通过MQTT控制设备
前言 一,微信小程序篇小程序下载(该功能为小程序篇基础功能源码) 实现功能概要 微信小程序通过扫描GPRS上的二维码,绑定GPRS设备.然后使用小程序通过GPRS远程控制开发板上的继电器, 远程显示单 ...
- HNOI2015总结
// 此博文为迁移而来,写于2015年4月21日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102vy9t.html 这次省 ...
- nginx 正向代理配置
需求场景:从以下俩张图可以比较直观的理解正向代理的作用(在其他文章中会表示为“http代理”,注意当前文档的配置不支持https代理) Nginx正向代理配置文件: server{ listen de ...
- JVM和ClassLoader
JVM和ClassLoader 2019-11-08 目录 1 JVM架构整体架构 1.1 类加载器子系统 1.1.1 加载 1.1.2 链接 1.1.3 初始化 1.2 运行时数据区(Runtime ...
- centos7 安装 bugfree3
. 安装apache yum install httpd . 安装mysql wget -i -c http://dev.mysql.com/get/mysql57-community-release ...
- Linux上DNS解析总是选择resolv.conf中第二位的DNS服务器IP地址
问题现象: 在Linux机器上,用户自建了一台DNS服务器.然后改动/etc/resolv.conf将其服务器IP地址添加到第一项.将阿里云的内网DNS放到第二位,然而在测试过程中发现telnet,p ...
- mvel语法指南[翻译]
mvel受到了java语法的启发,但是存在一些根本性的差异,mvel旨在使其成为更有效的表达式语言.比如直接支持集合.数组和字符串匹配,正则表达式的运算操作等. mvel2.x有以下几个部分组成: ...
- python开发笔记-Python3.7+Django2.2 Docker镜像搭建
目标镜像环境介绍: 操作系统:ubuntu16.04 python版本:python 3.7.4 django版本:2.2 操作步骤: 1. 本地安装docker环境(略)2. 拉取ubunut指定 ...