XV6学习笔记(1) : 启动与加载
XV6学习笔记(1)
1. 启动与加载
首先我们先来分析pc的启动。其实这个都是老生常谈了,但是还是很重要的(也不知道面试官考不考这玩意),
1. 启动的第一件事-bios
首先启动的第一件事就是运行bios,这个时候我们的机器位于实模式,也就是16位地址。这个时候能访问的空间只有1mb
- 就是设置cs寄存器的值为0xFFFF, ip的值为0x0000
- 这个就是bios的地址,然后我们会去运行bios执行各种对硬件的检查
- 但是xv6和之前的jos(也就是828)中都没有这样做,作为一个精简的os系统,
2. bootloader的汇编程序
我们的引导程序位于第一个扇区内。第一个扇区地址为0x7c00
。会在bios结束之后跳转到这里来
整个bootloader
程序分为两个部分。第一部分是汇编程序,第二部分则是c语言
第一件做的事情是关中断 + 清空寄存器
第二件就是打开A20.
打开A20是非常重要的一件事情。这是突破16位的关键。这里参考了别人的博客
我们具体来看 xv6 的实现代码
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1 movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64 seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2 movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60
这里
bootasm.S
用了两个方法seta20.1
和seta20.2
来实现通过 804x 键盘控制器打开 A20 gate。第一步是向 804x 键盘控制器的 0x64 端口发送命令。这里传送的命令是 0xd1,这个命令的意思是要向键盘控制器的 P2 写入数据。这就是 seta20.1 代码段所做的工作(具体的解释可以参看我在代码中写的注释)。
第二步就是向键盘控制器的 P2 端口写数据了。写数据的方法是把数据通过键盘控制器的 0x60 端口写进去。写入的数据是 0xdf,因为 A20 gate 就包含在键盘控制器的 P2 端口中,随着 0xdf 的写入,A20 gate 就被打开了。
接下来要做的就是进入“保护模式”了。
准备GDT表
进入保护模式之后,我们的寻址就要根据
段地址 + 段内偏移
来做了,所有这个全局段描述表非常关键啊GDT 表里的每一项叫做“段描述符”,用来记录每个内存分段的一些属性信息,每个“段描述符”占 8 字节,我们先来看一眼这个段描述符的具体结构:
GDT 也搞定了,接下来我们就要把我们刚刚在内存中设定好的 GDT 的位置告诉 CPU。CPU 单独为我们准备了一个寄存器叫做 GDTR 用来保存我们 GDT 在内存中的位置和我们 GDT 的长度。
GDTR 寄存器一共 48 位,其中高 32 位用来存储我们的 GDT 在内存中的位置,其余的低 16 位用来存我们的 GDT 有多少个段描述符。并且还专门提供了一个指令用来让我们把 GDT 的地址和长度传给 GDTR 寄存器,来看 xv6 的代码:
lgdt gdtdesc
而这个 gdtdesc 和 gdt 一起放在了 bootasm.S 文件的最底部,我们看一眼:
gdtdesc:
.word (gdtdesc - gdt - 1) # 16 位的 gdt 大小sizeof(gdt) - 1
.long gdt # 32 位的 gdt 所在物理地址
- 在xv6中,我们的cpu利用四个控制寄存器来进行一些状态控制,想要进入保护模式需要修改cr0寄存器
- PG 为 0 时代表只使用分段式,不使用分页式
为 1 是启用分页式 - PE 为 0 时代表关闭保护模式,运行在实模式下
为 1 则开启保护模式
最后看一下在xv6
中如何做到开启保护模式的
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
而这里其实就是把 cr0 寄存器的值 或上 $CR0_PE_ON的值。而. CR0_PE_ON = 0x0......1
这里的意思就是开启保护模式
- 进入c语言之前的一些汇编
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain
3. bootloader的c语言程序
- 首先去磁盘第一个扇区读取内核的ELF文件
- 判断是否是一个有效的ELF头文件
- 然后逐段把操作系统从磁盘中读到内核中
- 最后执行内核的程序,此后操作系统就交由内核处理
void
bootmain(void)
{
struct Proghdr *ph, *eph;
int i;
// read 1st page off disk
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++) {
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
for (i = 0; i < ph->p_memsz - ph->p_filesz; i++) {
*((char *) ph->p_pa + ph->p_filesz + i) = 0;
}
}
// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}
2. 执行内核
在kernel.ld
中有一些关于内核的设置
SECTIONS
{
/* Link the kernel at this address: "." means the current address */
. = 0xF0100000;
/* AT(...) gives the load address of this section, which tells
the boot loader where to load the kernel in physical memory */
.text : AT(0x100000) {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
PROVIDE(etext = .); /* Define the 'etext' symbol to this value */
.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
}
这里设置了内核的代码段位于内存中的0x100000
位置,而所对应的虚拟地址为0xF0100000
好了下面就可以去entry.S
看一下内核的代码了
1. 设置页表开启分页
- 对于64位机,CR3寄存器也从32位变成了64位,它的主要功能还是用来存放页目录表物理内存基地址,每当进程切换时,Linux就会把下一个将要运行进程的页目录表物理内存基地址等信息存放到CR3寄存器中。
- 首先开启4MB内存页。这里是通过设置cr4寄存器的PSE位来实现的
.globl entry
entry:
# Turn on page size extension for 4Mbyte pages
movl %cr4, %eax
orl $(CR4_PSE), %eax
movl %eax, %cr4
- 设置页目录开启页表
这里通过代码我们可以得到页表的基地址就在entrypgdir
中,这个变量可以在main.c中找到
开启页表就是通过调整cr0寄存器的位来实现的
# Set page directory
movl $(V2P_WO(entrypgdir)), %eax
movl %eax, %cr3
# Turn on paging.
movl %cr0, %eax
orl $(CR0_PG|CR0_WP), %eax
movl %eax, %cr0
// Boot page table used in entry.S and entryother.S.
// Page directories (and page tables), must start on a page boundary,
// hence the "__aligned__" attribute.
// Use PTE_PS in page directory entry to enable 4Mbyte pages.
__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
// Map VA's [0, 4MB) to PA's [0, 4MB)
[0] = (0) | PTE_P | PTE_W | PTE_PS,
// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};
//PAGEBREAK!
// Blank page.
将这些宏定义都转义过来我们看看这个页表的样子
unsigned int entrypgdir[1024] = {
[0] = 0 | 0x001 | 0x002 | 0x080, // 0x083 = 0000 1000 0011
[0x80000000 >> 22] = 0 | 0x001 | 0x002 | 0x080 // 0x083
};
当然这里只是一个临时页表。这里只有两个页表项 0x00000000
和 0x80000000
,而且两个页表项索引的内存物理地址都是 0 ~ 4MB
把虚拟地址空间的地址范围:0x80100000 -0x80500000
,映射到物理地址范围:0x00000000 - 0x00400000
上面。也可以把虚拟地址范围:0x00000000 - 0x00400000
,同样映射到物理地址范围:0x00000000~0x00400000
上面。任何不再这两个虚拟地址范围内的地址都会引起一个硬件异常。虽然只能映射这两块很小的空间,但是已经足够刚启动程序的时候来使用了。
这里的jos里地址就是0xF0100000,不过逻辑都是一模一样的
- 设置内核栈以及跳转到c语言到main.c
# Set up the stack pointer.
movl $(stack + KSTACKSIZE), %esp
# Jump to main(), and switch to executing at
# high addresses. The indirect call is needed because
# the assembler produces a PC-relative instruction
# for a direct jump.
mov $main, %eax
jmp *%eax
.comm stack, KSTACKSIZE
3. main.c
// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
main(void)
{
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kvmalloc(); // kernel page table
mpinit(); // collect info about this machine
lapicinit();
seginit(); // set up segments
cprintf("\ncpu%d: starting xv6\n\n", cpu->id);
picinit(); // interrupt controller
ioapicinit(); // another interrupt controller
consoleinit(); // I/O devices & their interrupts
uartinit(); // serial port
pinit(); // process table
tvinit(); // trap vectors
binit(); // buffer cache
fileinit(); // file table
iinit(); // inode cache
ideinit(); // disk
if(!ismp)
timerinit(); // uniprocessor timer
startothers(); // start other processors
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
userinit(); // first user process
// Finish setting up this processor in mpmain.
mpmain();
}
这里做了各种对于os的初始化。接下来我们将会看到 xv6 的内核是如何实现内存管理、进程管理、IO 操作等化操作系统所应该具有的功能,同时会结合jos也就是mit6.828进行对比一下。
XV6学习笔记(1) : 启动与加载的更多相关文章
- [置顶] iOS学习笔记47——图片异步加载之EGOImageLoading
上次在<iOS学习笔记46——图片异步加载之SDWebImage>中介绍过一个开源的图片异步加载库,今天来介绍另外一个功能类似的EGOImageLoading,看名字知道,之前的一篇学习笔 ...
- Flutter学习笔记(19)--加载本地图片
如需转载,请注明出处:Flutter学习笔记(19)--加载本地图片 上一篇博客正好用到了本地的图片,记录一下用法: 首先新建一个文件夹,这个文件夹要跟目录下 然后在pubspec.yaml里面声明出 ...
- Quartz.net 2.x 学习笔记03-使用反射加载定时任务
将定时任务信息存储在XML文件中,使用反射加载定时任务 首先新建一个MVC的空站点,使用NuGet添加对Quartz.net和Common.Logging.Log4Net1213的引用,同时使用NuG ...
- [OpenCV学习笔记3][图像的加载+修改+显示+保存]
正式进入OpenCV学习了,前面开始的都是一些环境搭建和准备工作,对一些数据结构的认识主要是Mat类的认识: [1.学习目标] 图像的加载:imread() 图像的修改:cvtColor() 图像的显 ...
- Android学习笔记_36_ListView数据异步加载与AsyncTask
一.界面布局文件: 1.加入sdcard写入和网络权限: <!-- 访问internet权限 --> <uses-permission android:name="andr ...
- 【OpenCV学习笔记之一】图像加载,修改及保存
加载图像(用cv::imread)imread功能是加载图像文件成为一个Mat对象 其中第一个参数表示图像文件名称第二个参数 表示加载的图像是什么类型 支持常见的三个参数值IMREAD_UNCHANG ...
- WMS学习笔记:1.尝试加载WMS
1.首先找一个可用的WMS栅格地图服务:http://demo.cubewerx.com/demo/cubeserv/cubeserv.cgi 获取GetCapabilities: http://de ...
- Android学习笔记_51_转android 加载大图片防止内存溢出
首先来还原一下堆内存溢出的错误.首先在SD卡上放一张照片,分辨率为(3776 X 2520),大小为3.88MB,是我自己用相机拍的一张照片.应用的布局很简单,一个Button一个ImageView, ...
- Laravel 学习笔记之 Composer 自动加载
说明:本文主要以Laravel的容器类Container为例做简单说明Composer的自动加载机制. Composer的自动加载机制 1.初始化一个composer项目 在一个空目录下compose ...
随机推荐
- Problem 1566 - C - Spanning Tree 动态最小生成树
Problem 1566 - C - Spanning Tree 给出一个联通图,然后每次加一条边,每次需要求最小生成树 1 #include <iostream> 2 #include ...
- jenkins send files or publish
1.创建一个自由风格项目 2.添加用户凭据 3.配置git 4.配置构建方式 这里选择 send files or execute command over SSH 5.配置远程发布脚本 6.构建 7 ...
- 通过MMIO的方式实现VIRTIO-BLK设备(一)
背景知识 什么是VIRTIO 使用完全虚拟化,Guest不加任何修改就可以运行在任何VMM上,VMM对于Guest是完全透明的.但每次I/O都将导致CPU在Guest模式与Host模式间切换,在I/O ...
- ROS2学习之旅(15)——编写简单的服务和客户节点(C++)
当节点使用服务进行通信时,发送数据请求的节点称为客户节点,响应请求的节点称为服务节点.请求和响应的结构由.srv文件决定. 本文的例子是一个简单的整数加法系统:一个节点请求两个整数的和,另一个节点响应 ...
- excel带格式复制python
openpyxl 复制cell单元格包括所有样式 target_cell.data_type = source_cell.data_type target_cell.fill = copy(sourc ...
- Ajax爬取动态数据和HTTPS自动默认证书
Ajax数据爬取 在spider爬取数据的过程中,有些网页的数据是利用Ajax动态加载出来的,所以,在网页源代码中可能不会看到这一部分的数据,因此,我们需要使用另外的方式进行数据多爬取. 以豆瓣电影的 ...
- C#/.NET/.NET Core学习视频汇总(持续更新ing)
前言: 之前有很多小伙伴在我的公众号后台留言问有没有C#/.NET/.NET Core这方面相关的视频推荐,我一般都会推荐他们去B站搜索一下.今天刚好有空收集了网上一些比较好的C#/.NET/.NET ...
- EasyUI:combotree(树形下拉框)复选框选中父节点(子节点的状态也全部选中)输入框中只显示父节点的文本值
参考: https://blog.csdn.net/weixin_43236850/article/details/100320564
- virtualbox结合nat和host-only设置固定ip的环境
需求 平时在做一些实验或学习的时候,比如rocketmq.kafaka.zookeeper等,需要在虚拟机上创建几个虚拟机组成集群来做实验:一般有两个要求: 虚拟机能访问网络,需要下载安装东西 虚拟机 ...
- Python开发篇——构建虚拟Python开发环境(Conda+Poetry)
前言 之前虽略有提及Python,但是没有实际地写点料.惭愧,惭愧,所以这次先起个头,讲讲如何构建虚拟Python开发环境.相信之前看过我博客的人可能会想:博主不会又要聊聊Docker吧?放心,不会. ...