学习路线
学习网站
https://objectkuan.gitbooks.io/ucore-docs/content/lab0.html
进行学习
实验步骤
- 启动操作系统的
bootloader
,用于了解操作系统启动前的状态
和要做的准备工作
,了解运行操作系统的硬件支持
,操作系统如何加载到内存中,理解两类中断–“外设中断”,“陷阱中断”等; - 物理内存管理子系统,用于理解x86分段/分页模式,了解操作系统
如何管理物理内存
; - 虚拟内存管理子系统,通过
页表机制
和换入换出(swap)机制
,以及中断-“故障中断”
、缺页故障处理
等,实现基于页的内存替换算法
; - 内核线程子系统,用于了解如何
创建相对与用户进程更加简单的内核态线程
,如果对内核线程进行动态管理等; - 用户进程管理子系统,用于了解
用户态进程创建、执行、切换和结束的动态管理过程
,了解在用户态通过系统调用得到内核态的内核服务的过程
; - 处理器调度子系统,用于理解操作系统的
调度过程
和调度算法
; 同步互斥与进程间通信子系统
,了解进程间如何进行信息交换和共享
,并了解同步互斥的具体实现以及对系统性能的影响,研究死锁产生的原因
,以及如何避免死锁;- 文件系统,
了解文件系统的具体实现
,与进程管理等的关系
,了解缓存对操作系统IO访问的性能改进,了解虚拟文件系统(VFS)、buffer cache和disk driver之间的关系。
项目下载地址
https://github.com/chyyuu/ucore_lab
实验环境
docker: 0xc4m3l/pwndocker
用到 qemu
https://objectkuan.gitbooks.io/ucore-docs/content/lab0/lab0_2_4_2_1_qemu_runtime_arguments.html
Mac 键盘切出 qemu control + option + x
视频中的知识点
https://www.bilibili.com/video/BV1js411b7vg
计算机的启动
内存加载硬盘的 bootloader
Bootloader 放在硬盘的第一个主引导扇区(512字节)
Bootloader 加载 OS(操作系统的代码,数据) 到内存
这个时候 CPU 控制权给了 bootloader
跳转到 操作系统运行
十六进制对应 中断类型码
应用程序无法直接访问磁盘设备 (利用磁盘读写的系统调用接口 -> 内核 -> 应用程序)
操作系统 –> 外设 中断,IO
操作系统 –> 应用程序 系统调用,异常
系统调用:(来源于应用程序)应用程序主动向操作系统发出服务请求。
异常:(来源于不良的应用程序)非法指令或其它花的处理状态(e.g.内存出错)。
中断:(来源于外设)来自不同的硬件设备的计时器和网络的中断。
产生的源头
- 中断:外设(键盘/鼠标/网卡/声卡/显卡,可以产生各种事件)
- 异常:应用程序意想不到的行为(e.g.异常,恶意程序,应用程序需要的资源未得到满足)
- 系统调用(system call):应用程序请求操作提供服务(e.g.打开/关闭/读写文件,发送网络包)
两个概念:用户态和内核态
- 用户态:
应用程序在执行的过程中,cpu所处于的一个特权级的状态,其特权级特别低,不能访问某些特殊的机器指令和io - 内核态:
操作系统运行过程中cpu所处于的一个状态,cpu可以执行任何的一条特权指令和io,可以完全的控制这个计算机系统
操作系统是一个软件。
实现 程序重定位,分段,分页,虚拟内存,按需分页虚拟内存
其实现高度依赖于硬件,必须知道内存架构,MMU(内存管理单元):硬件组件负责处理cpu的内存访问请求。
地址空间的定义
- 物理地址空间—与硬件直接对于(例如内存条所代表的主存)
- 逻辑地址空间—程序所看见的地址空间
逻辑地址空间 –> 落实到物理地址空间存在。程序分配一个 4G 虚拟内存,程序中的代码的位置,最终在 主存或者硬盘中通过操作系统规定
应用程序的生成
.o file – link –> .exe file(硬盘中)
.exe file – loader –> 运行 (放到内存中运行)逻辑地址
当cpu需要执行这条指令的过程如下:
ALU运算器需要这条指令的内容
cpu里面的mmu(内存管理单元)查找逻辑地址的映射表,找出逻辑地址和物理地址之间的映射
cpu控制器会从总线发送物理地址的内存内容的请求(就是指令的内容)
主存会把总线拿到的物理地址内存的内容传给cpu
其中,操作系统的作用是建立起逻辑地址和物理地址之间的映射。
压缩式(compression)碎片整理
利用拷贝 进行整合
内存拷贝前思考两个问题:
1)什么时候考虑内存的重定位是合适的
当程序处于等待的状态之中可以开始内存的重定位
2)考虑开销大不大
仅仅利用软件的移动实现开销是很大的
交换式(swap)碎片整理
当运行 p1 p2 p3 p4 时主存已经被装满。但是 p3运行中需要更多的内存,这个时候。我们可以将等待的程序数据放到磁盘中,然后扩充p3 需要的内存大小。这样保存程序的内存分配正常。
选择非连续内存分配
- 一个程序的物理地址空间是非连续的
- 更好的内存利用和管理 减少外碎片,内碎片的问题
- 允许共享代码与数据(共享库等…)
- 支持动态加载和动态链接
虚拟的逻辑地址空间是 连续的但是却分散在多个 物理地址空间中。
实现 数据隔离。动态库共享。 有效的管理分配,同时保证保护机制的实现
虚线左右通过 映射关系进行 映射
映射之后:位置不一样,大小也不一样
线性地址空间
操作系统的虚存管理之下每个运行的应用程序能访问的地址空间。
每个运行的应用程序都认为自己独享整个计算机系统的地址空间,这样可让多个运行的应用程序之间相互隔离。
逻辑地址空间
应用程序直接使用的地址空间
lab0
基础知识
OS Kernel的特征:
并发: 指一段时间内多个程序运行;而并行是指一个时间点上多个程序运行,要求多个CPU
共享:“同时”访问 或 互斥共享
虚拟:利用多道程序设计技术,让每一个用户都觉得有一个计算机专门为他服务
异步:程序的执行不是一步到底的,而是走走停停,向前推进的速度不可预知
但只要运行环境相同,OS要保证程序运行的结果也相同
在uCore内核使用了大量的双向循环链表结构来组织数据,包括空闲内存块列表、内存页链表、进程列表、设备链表、文件系统列表等的数据组织。
uCore的双向链表结构定义为:
1 | struct list_entry { |
链表节点list_entry没有包含传统的data数据域
插入(__list_add_after)
1 | static inline void |
*删除 list_del(list_entry_t listelm)
1 | static inline void |
访问链表节点所在的宿主数据结构
1 | //free_area是空闲块管理结构,free_area.free_list是空闲块链表头 |
lab1
实验
https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_1_goals.html
对于调试不能使用 pwndbg 只能使用 peda
知识点
初始化
BIOS 工作 :初始化底层硬件 ,保证机器能进行之后工作
加载
第一个扇
区 (bootloader) —> 512字节 实际大小没有512Booloader 做的事情
从
实模式(16位寻址空间)
切换到保护模式(32位寻址空间)
1MB -> 4GB读取
硬盘
中的 ucore 代码到内存
,控制权交给 ucore。(跳转过去执行 ucore )的代码执行 OS 代码
段机制管理
一种映射关系
通过 index
在段的表 Descriptor Table
中找到 对应起始位置和长度
起始位置 + offset(EIP) 得到对应的 地址
如果其实地址为 0 得到的地址就是物理地址
机制的建立 需要一个 大的数组(储存段描述符)
全局描述符表 (GDT)
由操作系统建立
GDT 由
bootloader
load GDT 是计算机得到 GDT 的描述计算机就能找到 GDT 的位置
利用内部寄存器
GDTR
保存相应地址信息段表的 index (16位) CS
高 13 位
对应 GDT 对应的索引 index低 2 位
0 1 2 3 对应特权级
内核 0 程序 3倒数第 3 位
0(GDT 表)1(LDT表)
x86 中断处理过程
每个中断
或者 异常
都有一个 中断服务 例程 (ISR) 关联
关联储存在 中断描述符表 (IDT)
大数组
IDT
的表信息 起始地址 长度 保存在 寄存器 IDTR 中
软中断 (trap) 陷阱
更具中断号 中断门 (陷阱门 trap) –> 进一步获得 端的选择址
取出 IDT
中的段选择址
(作为index) 从而在 GDT
找到对应的 段描述符
从而找到 中断服务例程 ISR
发生 中断
打断正在执行的 程序 执行 中断服务例程(ISR)
执行完才继续运行程序
段描述符 CS 最低两位设置 特权级 0内核 3用户
内核态 中断 栈地址还是在内核态
同一个 stack
用户态 中断 栈地址跳转到内核态
stack1 -- 跳转--> stack2
iret 中断例程返回
根据 cs eip 返回原来位置 还原 EFlAGS
ret + retf 函数返回
ret 只返回 EIP
retf 返回 EIP 恢复 cs 实现远程跳转
练习1:
题目
在此练习中,大家需要通过静态分析代码来了解:
- 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
问题1
这里利用 Make V=
对源代码进行了 编译 生成了 kernel
bootblock
sign
tools ucore.img
镜像
利用
gcc
编译成.o
的目标文件然后利用
ld
将目标文件 转换成 执行程序.out
文件利用
dd
将bootblock
和kernel
放到虚拟硬盘ucore.img
中
问题2
大小为 512 字节
阅读源码
lab1/tools/sign.c
发现 最后的 两个字节 为0x55 , 0xAA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int main(int argc, char *argv[]) {
struct stat st;
if (argc != 3) {
fprintf(stderr, "Usage: <input filename> <output filename>\n");
return -1;
}
if (stat(argv[1], &st) != 0) {
fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
return -1;
}
printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
if (st.st_size > 510) {
fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
return -1;
}
char buf[512];
memset(buf, 0, sizeof(buf));
FILE *ifp = fopen(argv[1], "rb");
int size = fread(buf, 1, st.st_size, ifp);
if (size != st.st_size) {
fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);
return -1;
}
fclose(ifp);
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) {
fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
return -1;
}
fclose(ofp);
printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
return 0;
}
练习2
gdb 调试
这里我们 使用 gdb 进行调试
我们可以利用 gdb 插件 pwngdb
+ peda
1 | cd ~/ |
题目
为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:
- 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
- 在初始化位置0x7c00设置实地址断点,测试断点正常。
- 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
- 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
回答
我们查看 tools/gdbinit
的内容
这里 我们如果直接 运行 make lab1-mon
我们就直接 断点在了 0x7c00
也就是 bootloader 的其实位置
根据汇编就能完成题目的 1.2
让比较 gdb 中的代码和 bootasm.S和 bootblock.asm 发现是基本一样的
发现程序 已经 运行到我们的 bootloader
了
说明我们的 qemu 已经加载了 对应的 bootloader
调试也就是简单的 gdb 调试就好
练习3
分析bootloader进入保护模式的过程。bootloader 实现的功能
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
bootloader 会开启 80386 的保护模式。进入32位寻址空间
开启A20,开启A20
初始化GDT表
使能和进入保护模式
保护模式和分段机制
实模式
加载完 bootloader PC处于 实模式 (16位模式)
当前访问的空间 不能超过 1mb.
通过修改 A20地址线
可以完成从 实模式 –> 保护模式 的转换。
保护模式
(32位模式)
可寻址高达4G字节的线性地址空间和物理地址空间,可访问64TB(有2^14个段,每个段最大空间为2^32字节)的逻辑地址空间,可采用分段存储管理机制和分页存储管理机制。不仅为存储共享和保护提供了硬件支持,而且为实现虚拟存储提供了硬件支持。
通过 4个特权级和完善的特权检查机制 实现 资源共享和代码安全
保护模式下,有两个段表 :
GDT(Global Descriptor Table)
和LDT(Local Descriptor Table)
,每一张段表可以包含8192 (2^13)个描述符[1],因而最多可以同时存在2 * 2^13 = 2^14个段。逻辑地址空间看起来很大,但实际上段并不能扩展物理地址空间,很大程度上各个段的地址空间是相互重叠的。
分段存储管理机制
保护模式下启动 分段存储管理机制
内存划分成以
起始地址
和长度限制
这两个二维参数表示的内存块,这些内存块就称之为段(Segment)。分段机涉及4个关键内容:
- 逻辑地址
- 段描述符(描述段的属性)
- 段描述符表(包含多个段描述符的“数组”)
- 段选择子(段寄存器,用于定位段描述符表中表项的索引)。
逻辑地址(Logical Address,应用程序员看到的地址)到物理地址(Physical Address, 实际的物理内存地址)
分段地址转换
段描述符表
的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址
加上段偏移值
,形成线性地址(Linear Address)。分页地址转换 线性地址转换为物理地址。 (这一步是可选的,由操作系统决定是否需要)
为了使得分段存储管理机制正常运行,需要建立好 段描述符
和 段描述符表
段描述符
- 段基地址:规定线性地址空间中段的起始地址。
- 段界限:规定段的大小。 段界限可以是以字节为单位或以4K字节(页表)为单位。
- 段属性:确定段的各种性质。
保护模式下的特权级
在保护模式下,特权级总共有4个,编号从0(最高特权)到3(最低特权)。
x86 CPU都是在一个特定的特权级下运行的
关于A20 Gate
开启A20是为了能够在保护模式下正常访问超出1MB的内存且不会发生回卷,否则在保护模式下只能访问奇数位的内存
理论上讲,我们只要操作8042芯片的输出端口(64h)的bit 1,就可以控制A20 Gate,但实际上,当你准备向8042的输入缓冲区里写数据时,可能里面还有其它数据没有处理,所以,我们要首先禁止键盘操作,同时等待数据缓冲区中没有数据以后,才能真正地去操作8042打开或者关闭A20 Gate。
打开A20 Gate的具体步骤大致如下:
- 等待8042 Input buffer为空
- 发送Write 8042 Output Port (P2) 命令到8042 Input buffer
- 等待8042 Input buffer为空
- 将8042 Output Port(P2) 对应字节的第2位置1,然后写入8042 Input buffer
初始化 GDT 表
GDT初始化的代码:
1 | #define SEG_NULLASM \ |
CPU通过lgdt指令读入GDT的地址,之后我们就可以使用GDT了。
1 | .set PROT_MODE_CSEG, 0x8 |
如何使能和进入保护模式
将cr0的最低为设置为1
1 | movl %cr0, %eax |
练习4
问题
分析bootloader加载ELF格式的OS的过程。
bootmain.c
了解bootloader如何加载ELF文件。
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载ELF格式的OS?
硬盘访问概述
bootloader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。
bootloader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。
当前 硬盘数据是储存到硬盘扇区中,一个扇区大小为512字节。读一个扇区的流程
(可参看boot/bootmain.c中的readsect函数实现)大致如下:
- 等待磁盘准备好
- 发出读取扇区的命令
- 等待磁盘准备好
- 把磁盘扇区数据读到指定内存
bootloader如何读取硬盘扇区
利用到 readsect
函数读取扇区 用到 outb 指令
1 | /* waitdisk - wait for disk ready */ |
ELF文件格式概述
ELF 文件结构 elf.h
1 | struct elfhdr { |
程序 头表结构 属于什么节,节在文件当中起始位置,虚拟地址,物理地址,文件长度,虚拟空间长度等。
1 | struct proghdr { |
bootloader是如何加载ELF格式的OS
主要会利用到的代码
1 | bootmain(void) { |
练习5
问题
我们需要在lab1中完成 lab1/kern/debug/kdebug.c
中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。
显示结果
1 | …… |
布局图
1 | +| 栈底方向 | 高位地址 |
结果
根据代码的 注释 编写 print_stackframe(void)
函数
1 | /* LAB1 YOUR CODE : STEP 1 */ |
更具注释完成代码
1 | void print_stackframe(void) { |
练习6
问题
请完成编码工作和回答如下问题:
- 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
- 请编程完善
kern/trap/trap.c
中对中断向量表进行初始化的函数idt_init
。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c
生成,使用trap.c中声明的vectors数组即可。 - 请编程完善
trap.c
中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
问题1
表结构如下,中断描述符表一个表项占8个字节
1 | struct gatedesc { |
16~31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。
问题2
要求
1 | /* LAB1 YOUR CODE : STEP 2 */ |
实现代码
1 | void idt_init(void) { |
问题3
实现
1 | static uint16_t tick = 0; |