XV6学习笔记(1)

1. 启动与加载

首先我们先来分析pc的启动。其实这个都是老生常谈了,但是还是很重要的(也不知道面试官考不考这玩意),

1. 启动的第一件事-bios

首先启动的第一件事就是运行bios,这个时候我们的机器位于实模式,也就是16位地址。这个时候能访问的空间只有1mb

  1. 就是设置cs寄存器的值为0xFFFF, ip的值为0x0000
  2. 这个就是bios的地址,然后我们会去运行bios执行各种对硬件的检查
  3. 但是xv6和之前的jos(也就是828)中都没有这样做,作为一个精简的os系统,

2. bootloader的汇编程序

我们的引导程序位于第一个扇区内。第一个扇区地址为0x7c00。会在bios结束之后跳转到这里来

整个bootloader程序分为两个部分。第一部分是汇编程序,第二部分则是c语言

  1. 第一件做的事情是关中断 + 清空寄存器

  2. 第二件就是打开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.1seta20.2 来实现通过 804x 键盘控制器打开 A20 gate。

    第一步是向 804x 键盘控制器的 0x64 端口发送命令。这里传送的命令是 0xd1,这个命令的意思是要向键盘控制器的 P2 写入数据。这就是 seta20.1 代码段所做的工作(具体的解释可以参看我在代码中写的注释)。

    第二步就是向键盘控制器的 P2 端口写数据了。写数据的方法是把数据通过键盘控制器的 0x60 端口写进去。写入的数据是 0xdf,因为 A20 gate 就包含在键盘控制器的 P2 端口中,随着 0xdf 的写入,A20 gate 就被打开了。

    接下来要做的就是进入“保护模式”了。

  3. 准备GDT表

    进入保护模式之后,我们的寻址就要根据段地址 + 段内偏移来做了,所有这个全局段描述表非常关键啊

    GDT 表里的每一项叫做“段描述符”,用来记录每个内存分段的一些属性信息,每个“段描述符”占 8 字节,我们先来看一眼这个段描述符的具体结构:

  1. 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 所在物理地址
  1. 在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

这里的意思就是开启保护模式

  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语言程序

  1. 首先去磁盘第一个扇区读取内核的ELF文件
  2. 判断是否是一个有效的ELF头文件
  3. 然后逐段把操作系统从磁盘中读到内核中
  4. 最后执行内核的程序,此后操作系统就交由内核处理
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. 设置页表开启分页

  1. 对于64位机,CR3寄存器也从32位变成了64位,它的主要功能还是用来存放页目录表物理内存基地址,每当进程切换时,Linux就会把下一个将要运行进程的页目录表物理内存基地址等信息存放到CR3寄存器中。

  1. 首先开启4MB内存页。这里是通过设置cr4寄存器的PSE位来实现的
.globl entry
entry:
# Turn on page size extension for 4Mbyte pages
movl %cr4, %eax
orl $(CR4_PSE), %eax
movl %eax, %cr4
  1. 设置页目录开启页表

这里通过代码我们可以得到页表的基地址就在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
};

当然这里只是一个临时页表。这里只有两个页表项 0x000000000x80000000,而且两个页表项索引的内存物理地址都是 0 ~ 4MB

把虚拟地址空间的地址范围:0x80100000 -0x80500000,映射到物理地址范围:0x00000000 - 0x00400000上面。也可以把虚拟地址范围:0x00000000 - 0x00400000,同样映射到物理地址范围:0x00000000~0x00400000上面。任何不再这两个虚拟地址范围内的地址都会引起一个硬件异常。虽然只能映射这两块很小的空间,但是已经足够刚启动程序的时候来使用了。

这里的jos里地址就是0xF0100000,不过逻辑都是一模一样的

  1. 设置内核栈以及跳转到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) : 启动与加载的更多相关文章

  1. [置顶] iOS学习笔记47——图片异步加载之EGOImageLoading

    上次在<iOS学习笔记46——图片异步加载之SDWebImage>中介绍过一个开源的图片异步加载库,今天来介绍另外一个功能类似的EGOImageLoading,看名字知道,之前的一篇学习笔 ...

  2. Flutter学习笔记(19)--加载本地图片

    如需转载,请注明出处:Flutter学习笔记(19)--加载本地图片 上一篇博客正好用到了本地的图片,记录一下用法: 首先新建一个文件夹,这个文件夹要跟目录下 然后在pubspec.yaml里面声明出 ...

  3. Quartz.net 2.x 学习笔记03-使用反射加载定时任务

    将定时任务信息存储在XML文件中,使用反射加载定时任务 首先新建一个MVC的空站点,使用NuGet添加对Quartz.net和Common.Logging.Log4Net1213的引用,同时使用NuG ...

  4. [OpenCV学习笔记3][图像的加载+修改+显示+保存]

    正式进入OpenCV学习了,前面开始的都是一些环境搭建和准备工作,对一些数据结构的认识主要是Mat类的认识: [1.学习目标] 图像的加载:imread() 图像的修改:cvtColor() 图像的显 ...

  5. Android学习笔记_36_ListView数据异步加载与AsyncTask

    一.界面布局文件: 1.加入sdcard写入和网络权限: <!-- 访问internet权限 --> <uses-permission android:name="andr ...

  6. 【OpenCV学习笔记之一】图像加载,修改及保存

    加载图像(用cv::imread)imread功能是加载图像文件成为一个Mat对象 其中第一个参数表示图像文件名称第二个参数 表示加载的图像是什么类型 支持常见的三个参数值IMREAD_UNCHANG ...

  7. WMS学习笔记:1.尝试加载WMS

    1.首先找一个可用的WMS栅格地图服务:http://demo.cubewerx.com/demo/cubeserv/cubeserv.cgi 获取GetCapabilities: http://de ...

  8. Android学习笔记_51_转android 加载大图片防止内存溢出

    首先来还原一下堆内存溢出的错误.首先在SD卡上放一张照片,分辨率为(3776 X 2520),大小为3.88MB,是我自己用相机拍的一张照片.应用的布局很简单,一个Button一个ImageView, ...

  9. Laravel 学习笔记之 Composer 自动加载

    说明:本文主要以Laravel的容器类Container为例做简单说明Composer的自动加载机制. Composer的自动加载机制 1.初始化一个composer项目 在一个空目录下compose ...

随机推荐

  1. Spring Boot整合MybatisPlus逆向工程(MySQL/PostgreSQL)

    MyBatis-Plus是MyBatis的增强工具,Generator通过MyBatis-Plus快速生成Entity.Mapper.Mapper XML.Service.Controller等模块的 ...

  2. 『无为则无心』Python函数 — 27、Python函数的返回值

    目录 1.返回值概念 2.return关键字的作用 3.返回值可以返回的数据类型 4.函数如何返回多个值 5.fn5 和 fn5()的区别 6.总结: 1.返回值概念 例如:我们去超市购物,比如买饮料 ...

  3. namenode和datanode启动失败

    1.namenode启动失败,查看错误原因,是无法格式化,再看日志,根据日志提示,清空对应的目录,即可解决这个问题. 2.datanode启动失败: Can't open /var/run/cloud ...

  4. [心得体会]spring事务源码分析

    spring事务源码分析 1. 事务的初始化注册(从 @EnableTransactionManagement 开始) @Import(TransactionManagementConfigurati ...

  5. 2012年第三届蓝桥杯C/C++程序设计本科B组省赛 取球博弈

    2012年第三届蓝桥杯C/C++程序设计本科B组省赛 取球博弈 题目描述 **取球博弈 今盒子里有n个小球,A.B两人轮流从盒中取球,每个人都可以看到另一个人取了多少个,也可以看到盒中还剩下多少个,并 ...

  6. WPF教程五:附加依赖项属性

    附加依赖项属性是一个属性本来不属于对象自己,但是某些特定场景其他的对象要使用该对象在这种场景下的值.这个值只在这个场景下使用.基于这个需求设计出来的属性.这里主要涉及到一个解耦问题.最大的优势是在特定 ...

  7. [期望DP][纪中]【2010集训队出题】彩色圆环

    彩色圆环 感谢名单 十分感谢 JA_Ma 为我讲解了 \(T1\) 的 期望DP 的思想和推论. 十分感谢 SSL_LYF 为我解答了 \(T1\) 的 期望DP 的概率的大小问题. 十分感谢 SSL ...

  8. ARC 122 简要题解

    ARC 122 简要题解 传送门 A - Many Formulae 考虑对于每个数分别算其贡献. 通过枚举该数前面的符号,借助一个非常简单的 \(\mathrm{DP}\)(\(f_{i,0/1}\ ...

  9. 海亮NOIP集训-每日总结

    [总结] xzh 2021暑假每日结 2021年7月12日 内容主题 DP,树型DP(讲解人:王修涵) 考场题目总结 T1: 考场简单想法: 算出两两点间距离,贪心,所用时间 \(1.5h\) 左右. ...

  10. mysql中函数cast使用

    CAST函数语法规则是:Cast(字段名 as 转换的类型 ),其中类型可以为: CHAR[(N)] 字符型DATE 日期型DATETIME 日期和时间型DECIMAL float型SIGNED in ...