这才是计科之 Onix & XV6 源码分析(1、XV6-x86的启动)

这才是计科之 Onix & XV6 源码分析(1、XV6-x86的启动)

前言

Onix是一款相对于XV6来说功能更为健全的单核OS,由于功能更加完善,Onix也更加复杂。代码阅读起来会比较绕。

XV6是一款简单易读的多核操作系统,但其功能相对Onix来说更为简陋,比如Onix对物理内存的管理采用了位图、内核内存和进程相关的内存进行了分开管理,页目录使用单独的内核内存,没有和页表、页框混用等。而XV6显得非常简陋。尽管XV6的实验可以弥补部分缺陷。

Onix操作系统也实现了bootloader,对于将OS加载到内存的操作,Onix是采用汇编进行内核加载,并且在加载内核前,还会进行一个内存探测的操作,所以Onix的bootloader稍微有些复炸。而XV6操作系统的启动操作写的非常简洁,加载内核的操作采用的是C语言的形式,非常利于阅读学习,但是XV6不会进行内存探测。为求方便,本文主要叙述XV6的启动流程。

Onix相关链接:

XV6-x86的github链接:

Makefile & kernel.ld文件的分析

在叙述os启动前,必须要了解其Makefile是怎么写的。同时,在了解bootmain从镜像中加载os的代码到内存中前,因为os是elf格式,所以我们需要了解os的link脚本是怎么布局的。以便我们能更好掌握os的内存布局。

Makefile

这里贴出makefile比较关键的代码:

makefile 复制代码
# 利用dd命令制作OS镜像,依赖bootblock和kernel,
# 首先划分了10000个扇区
# 然后将bootblock写到了第0号扇区
# 最后从1号扇区开始,填入OS的代码文件。
xv6.img: bootblock kernel
	dd if=/dev/zero of=xv6.img count=10000
	dd if=bootblock of=xv6.img conv=notrunc
	dd if=kernel of=xv6.img seek=1 conv=notrunc

# 单独产生os的bootloader模块,并且该模块是使用$(OBJCOPY)产生,
# 所以没有elf文件头信息,只是单纯的二进制可执行文件。并且$(LD)规定
# 代码的入口点是start、并且从地址0x7C00(物理地址)开始,最最的文件名是bootblock
bootblock: bootasm.S bootmain.c
	$(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c
	$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootasm.S
	$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o
	$(OBJDUMP) -S bootblock.o > bootblock.asm
	$(OBJCOPY) -S -O binary -j .text bootblock.o bootblock
	./sign.pl bootblock

# 产生AP cpu的boot代码
# 指定程序的加载地址是0x7C00(物理地址)
# 可执行代码的格式和bootblock相同,纯粹的二进制程序,没有elf的头部信息
# 但最终输出的entryother会和kernel合并
entryother: entryother.S
	$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c entryother.S
	$(LD) $(LDFLAGS) -N -e start -Ttext 0x7000 -o bootblockother.o entryother.o
	$(OBJCOPY) -S -O binary -j .text bootblockother.o entryother
	$(OBJDUMP) -S bootblockother.o > entryother.asm

# 产生第一个进程,init进程的boot代码
# 同样以$(OBJCOPY)提取纯粹的可执行代码,没有elf头部信息,输出文件名为initcode
# 指定程序的加载地址是0(虚拟地址)
initcode: initcode.S
	$(CC) $(CFLAGS) -nostdinc -I. -c initcode.S
	$(LD) $(LDFLAGS) -N -e start -Ttext 0 -o initcode.out initcode.o
	$(OBJCOPY) -S -O binary initcode.out initcode
	$(OBJDUMP) -S initcode.o > initcode.asm

# 产生内核的elf可执行文件
kernel: $(OBJS) entry.o entryother initcode kernel.ld
	$(LD) $(LDFLAGS) -T kernel.ld -o kernel entry.o $(OBJS) -b binary initcode entryother
	$(OBJDUMP) -S kernel > kernel.asm
	$(OBJDUMP) -t kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > kernel.sym

这里编译器、链接器的选项具体作用读者可以自行百度,这里只阐述比较关键的部分。

首先是利用dd命令制作xv6.img镜像,从代码中可以很清楚的看到,bootblock填到了第0号扇区、kernel填到了1号以及之后的扇区。bootblock作用就是使cpu从实时模式转换为保护模式,然后将kernel从磁盘上加载到内存。这里要注意一个特殊的数字0x7C00,它出现在生成bootblock二进制文件的$(LD)阶段,这里暗示了bootblock代码在加载进内存时应该被放在0x7C00的位置。事实也是如此,在BIOS完成硬件初始化后,就会将第0号扇区(一个扇区一般就是512字节)的512字节代码加载到内存的0x7C00的位置,然后BIOS就会让eip指向0x7C00的位置,去执行bootasm.S里面的汇编代码。

这里有个关键点:在使用$(LD)命令生成bootblock.o时,命令参数部分bootasm.o放在bootmain.o前面,会导致链接时,bootasm.o代码就会靠前,这样在eip执行0x7C00位置的代码时,一定是从start开始。

bootblock是由(OBJCOPY)生成,(OBJCOPY)的作用就是去除elf文件中的各种头部,因为BIOS只负责从第0号扇区加载bootblock,不会解析elf文件,所以,$(OBJCOPY)去提取纯粹的二进制是非常有必要的!

bootblock具体细节下面会详细探讨

kernel.ld

这里贴出kernel.ld比较关键的代码:

link 复制代码
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)

SECTIONS
{
	/* Link the kernel at this address: "." means the current address */
        /* Must be equal to KERNLINK */
	. = 0x80100000; // 定义代码起始的虚拟地址

	.text : AT(0x100000) {  // 定义了起始加载地址
		*(.text .stub .text.* .gnu.linkonce.t.*)
	}

  /*
    省略...
  */
}

从kernel的链接脚本我们可以看到,kernel的起始虚拟内存地址是0x8010 0000,内核实际加载的物理地址是x100000,由AT定义。这里我反复标注了 虚拟地址 / 物理地址 ,这两者一定要分清!

从实时模式到保护模式

本段代码实现位于xv6的bootasm.S文件。具体细节如下,xv6使用的AT&T的汇编,还是比较好懂的,读者有疑问的话,可以百度去搜相关指令的作用。英文注释已经非常详细,我就直接引用了。

arm 复制代码
#include "asm.h"
#include "memlayout.h"
#include "mmu.h"

# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli                         # BIOS enabled interrupts; disable

  # Zero data segment registers DS, ES, and SS.
  xorw    %ax,%ax             # Set %ax to zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

  # 硬件相关,主线是OS的启动,该部分不深究也没影响,其实就是一个固定步骤。
  # Physical address line A20 is tied to zero so that the first PCs 
  # with 2 MB would run software that assumed 1 MB.  Undo that.
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

  # Switch from real to protected mode.  Use a bootstrap GDT that makes
  # virtual addresses map directly to physical addresses so that the
  # effective memory map doesn't change during the transition.
  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE, %eax
  movl    %eax, %cr0
 ########################### 以下正式进入32位保护模式
//PAGEBREAK!
  # Complete the transition to 32-bit protected mode by using a long jmp
  # to reload %cs and %eip.  The segment descriptors are set up with no
  # translation, so that the mapping is still the identity mapping.
  ljmp    $(SEG_KCODE<<3), $start32

.code32  # Tell assembler to generate 32-bit code now.
start32:
  # Set up the protected-mode data segment registers
  movw    $(SEG_KDATA<<3), %ax    # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %ss                # -> SS: Stack Segment
  movw    $0, %ax                 # Zero segments not ready for use
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS

  # Set up the stack pointer and call into C.
  movl    $start, %esp            # 这里将esp栈设置到了start,由于栈向低地址处增长,所以刚好和bootasm文件的代码背道而驰。
  call    bootmain

  # If bootmain returns (it shouldn't), trigger a Bochs
  # breakpoint if running under Bochs, then loop.
  movw    $0x8a00, %ax            # 0x8a00 -> port 0x8a00
  movw    %ax, %dx
  outw    %ax, %dx
  movw    $0x8ae0, %ax            # 0x8ae0 -> port 0x8a00
  outw    %ax, %dx
spin:
  jmp     spin

# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULLASM                             # null seg
  SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)   # code seg
  SEG_ASM(STA_W, 0x0, 0xffffffff)         # data seg

gdtdesc:
  .word   (gdtdesc - gdt - 1)             # sizeof(gdt) - 1
  .long   gdt                             # address gdt

这里先普及一下实时模式和保护模式的区别:

  • 实时模式:为兼容以前的PC。特点是:寄存器都是16位。寻址方式:16位的段寄存器 + 16位的偏移寄存器,最大寻址范围是20位。

  • 保护模式:现代CPU的寻址方式。特点是:寄存器有16位、32为、64位。寻址方式:段描述符 + 32位偏移寄存器。最大寻址范围4G+。

bootasm.S是操作系统被加载内存前,最先开始执行的代码。BIOS是运行在实时模式下的程序,只拥有1M的寻址空间(boot代码被加载到0x7C00 < 1M 就能证明存在1M的限制),所以在cpu拥有4G寻址空间前,还需要进行一些初始化操作,

从实时模式 -> 保护模式的转变流程非常固定,在xv6的bootasm中实现如下:

  1. 对于多核处理器,最先启动的CPU(我们称为BSP(bootstrap processor)),BSP一上电就是实时模式。其余的从处理器(我们称为AP)在后面的内核初始化阶段会被BSP依次唤醒并初始化。

  2. 关中断。清空各种段寄存器,包括ds、es、ss。

  3. 打开A20地址线.

  4. 加载全局描述符表。即使用lgdt指令将表的地址和大小放在GDTR中。(这里的全局描述符表是临时的,后面内核初始化会更换一张gdt表,那张表更加完善。

  5. 将CR0寄存器第0位设置为1(PE位),此时正式转换成保护模式。

  6. 使用ljmp(长跳转指令)指令刷新段寄存器,跳到start32。

    # xv6对ljmp的注释如下:
    # Complete the transition to 32-bit protected mode by using a long jmp
    # to reload %cs and %eip. 
    
  7. 初始化数据段、栈段寄存器,将esp设置到0x7C00处,跳到bootmain函数中,该函数会将XV6 OS加载到内存。

相关的结构如下:

段描述符(Descriptor),描述一段地址的特性,包括基地址、范围、权限、粒度(范围以字节为单位还是以4K为单位)、类型(代码/数据)等信息:

全局描述符表,由许多个8字节段描述符组成的表:

全局描述符表寄存器,其基地址 的内容是全局描述符表的首地址,界限是全局描述符表的大小:

段选择子,【代码、数据、栈等】段使用了哪个段描述符,索引号指使用段描述符在全局描述符表的偏移(8字节为单位),第2位指明是全局描述符还是局部描述符,0~1位指示段的特权级:

从低特权级的段空间 跳到 高特权级的段空间就会发生cpu特权级的切换,cpu就是通过全局描述符表来确定一个段的特权级。最典型的就是用户进程调用系统调用产生的特权级切换,这中间涉及查tss段、切栈等复杂操作,我们后面在进行详细的讨论。

对于GDT的详细描述,这里推荐两篇博客,这两篇博客写的真的非常好!相信阅读之后,对全局描述符会有一个清晰的认识,全局描述符的几幅图片也是取自这两篇文章,如有侵权,可告知删除:

详细介绍了段描述符各个位的作用

可以作为扩展,里面介绍了局部描述符的作用

正式将XV6加载到内存

分析该部分细节前,先需要了解一下elf文件的格式,我们重点关注elf文件的ELF header和Program header table。直接上各个字段的描述:

引用自博客https://zhuanlan.zhihu.com/p/165336511如有侵权,可告知删除:

cpp 复制代码
#define ELF_MAGIC 0x464C457FU  // "\x7FELF" in little endian

// ELF 文件的头部
struct elfhdr {
  uint magic;       // 4 字节,为 0x464C457FU(大端模式)或 0x7felf(小端模式)
                      // 表明该文件是个 ELF 格式文件

  uchar elf[12];    // 12 字节,每字节对应意义如下:
                    //     0 : 1 = 32 位程序;2 = 64 位程序
                    //     1 : 数据编码方式,0 = 无效;1 = 小端模式;2 = 大端模式
                    //     2 : 只是版本,固定为 0x1
                    //     3 : 目标操作系统架构
                    //     4 : 目标操作系统版本
                    //     5 ~ 11 : 固定为 0

  ushort type;      // 2 字节,表明该文件类型,意义如下:
                    //     0x0 : 未知目标文件格式
                    //     0x1 : 可重定位文件
                    //     0x2 : 可执行文件
                    //     0x3 : 共享目标文件
                    //     0x4 : 转储文件
                    //     0xff00 : 特定处理器文件
                    //     0xffff : 特定处理器文件

  ushort machine;   // 2 字节,表明运行该程序需要的计算机体系架构,
                    // 这里我们只需要知道 0x0 为未指定;0x3 为 x86 架构

  uint version;     // 4 字节,表示该文件的版本号

  uint entry;       // 4 字节,该文件的入口地址,没有入口(非可执行文件)则为 0

  uint phoff;       // 4 字节,表示该文件的"程序头部表"相对于文件的位置,单位是字节

  uint shoff;       // 4 字节,表示该文件的"节区头部表"相对于文件的位置,单位是字节

  uint flags;       // 4 字节,特定处理器标志

  ushort ehsize;    // 2 字节,ELF文件头部的大小,单位是字节

  ushort phentsize; // 2 字节,表示程序头部表中一个入口的大小,单位是字节

  ushort phnum;     // 2 字节,表示程序头部表的入口个数,
                    // phnum * phentsize = 程序头部表大小(单位是字节)

  ushort shentsize; // 2 字节,节区头部表入口大小,单位是字节

  ushort shnum;     // 2 字节,节区头部表入口个数,
                    // shnum * shentsize = 节区头部表大小(单位是字节)

  ushort shstrndx;  // 2 字节,表示字符表相关入口的节区头部表索引
};

// 程序头表
struct proghdr {
  uint type;        // 4 字节, 段类型
                    //         1 PT_LOAD : 可载入的段
                    //         2 PT_DYNAMIC : 动态链接信息
                    //         3 PT_INTERP : 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小
                    //         4 PT_NOTE : 指定辅助信息的位置和大小
                    //         5 PT_SHLIB : 保留类型,但具有未指定的语义
                    //         6 PT_PHDR : 指定程序头表在文件及程序内存映像中的位置和大小
                    //         7 PT_TLS : 指定线程局部存储模板
  uint off;         // 4 字节, 段的第一个字节在文件中的偏移
  uint vaddr;       // 4 字节, 段的第一个字节在内存中的虚拟地址
  uint paddr;       // 4 字节, 段的第一个字节在内存中的物理地址(适用于物理内存定位型的系统)
  uint filesz;      // 4 字节, 段在文件中的长度
  uint memsz;       // 4 字节, 段在内存中的长度
  uint flags;       // 4 字节, 段标志
                    //         1 : 可执行
                    //         2 : 可写入
                    //         4 : 可读取
  uint align;       // 4 字节, 段在文件及内存中如何对齐
};

如果感兴趣的话,可以参考文章:ELF文件格式的详解,这篇文章讲解的更为详细,但是对于现阶段来说,可以不用了解太仔细,知道elf文件的ELF header和Program header table各个字段的作用就足够你继续学习XV6操作系统。

对kernel elf文件解析的主流程:

cpp 复制代码
void
bootmain(void)
{
  struct elfhdr *elf;
  struct proghdr *ph, *eph;
  void (*entry)(void);
  uchar* pa;
  // 预留足够空间
  // 46K
  elf = (struct elfhdr*)0x10000;  // scratch space

  // 以0为偏移读4096个字节,读elf文件头
  // Read 1st page off disk
  readseg((uchar*)elf, 4096, 0);

  // 判断elf文件魔数。
  // Is this an ELF executable?
  if(elf->magic != ELF_MAGIC)
    return;  // let bootasm.S handle error

  // 定位到Program header table
  // Load each program segment (ignores ph flags).
  ph = (struct proghdr*)((uchar*)elf + elf->phoff);
  // 程序头部表个数
  eph = ph + elf->phnum;

  // 一个段一个段的读
  for(; ph < eph; ph++){  // 以struct proghdr为单位自增。
    // 应该加载到的物理内存,xv6中是0x100000
    pa = (uchar*)ph->paddr;
    // 读取整个段到pa中
    readseg(pa, ph->filesz, ph->off);
    if(ph->memsz > ph->filesz)
      // mem大小比file大小大,多余的补零
      stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
  }

  // Call the entry point from the ELF header.
  // Does not return!
  entry = (void(*)(void))(elf->entry);  // 在xv6的kernel.ld中描述为_start
  entry();
}

前半部分做的操作就是不断从kernel的elf文件中按照程序头将各个段读取到内存。后面通过elf头中的entry,该字段保存内核入口点_start(这可以通过阅读kernel.ld文件来证明),去执行_start开始的代码,这里要提醒读者的是,到目前为止,对cpu来说状态是:保护模式 & 已经装填了全局描述符 & 一切地址皆是物理地址。而kernel的elf文件中代码都是使用的(0x8010 0000开始的)虚拟地址,所以我们后面在entry.S中会看到,在给_start赋值前,会通过一个宏将真正的入口地址的虚拟地址转换为XV6加载到内存的物理地址。从而我们在物理地址寻址模式下,可以利用_start准确跳转到xv6的入口代码。

需要注意的是:在执行bootasm汇编代码时,esp的位置是不确定的,唯一能确定的是esp在1M空间之内。在bootasm汇编最后才会将esp挪到0x7c00的位置!

在bootmain执行完毕后,内存布局如下:

接下来深入分析一下从磁盘读取文件的细节,这些内容都是和硬件相关的,通过操作硬件寄存器来实现读写磁盘,这部分深入下去也是挺让人头大的,作者也是菜鸟一个,也就力所能及的叙述一些自己明白的东西吧。

cpp 复制代码
void
waitdisk(void)
{
  // Wait for disk ready.
  while((inb(0x1F7) & 0xC0) != 0x40)
    ;
}

// readsect也引用自https://zhuanlan.zhihu.com/p/165336511,如有侵权,可联系我删除
// Read a single sector at offset into dst.
void
readsect(void *dst, uint offset)
{
  // Issue command.
  waitdisk();
  outb(0x1F2, 1);   // count = 1          // 要读取的扇区数量 count = 1
  outb(0x1F3, offset);                    // 扇区 LBA 地址的 0-7 位
  outb(0x1F4, offset >> 8);               // 扇区 LBA 地址的 8-15 位
  outb(0x1F5, offset >> 16);              // 扇区 LBA 地址的 16-23 位
  outb(0x1F6, (offset >> 24) | 0xE0);     // offset | 11100000 保证高三位恒为 1
                                          //         第7位     恒为1
                                          //         第6位     LBA模式的开关,置1为LBA模式
                                          //         第5位     恒为1
                                          //         第4位     为0代表主硬盘、为1代表从硬盘
                                          //         第3~0位   扇区 LBA 地址的 24-27 位
  outb(0x1F7, 0x20);  // cmd 0x20 - read sectors  // 20h为读,30h为写

  // Read data.
  waitdisk();
  insl(0x1F0, dst, SECTSIZE/4); // 读的时候以4字节位单位读,所以需要扇区除以4,代表要读的次数
}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked.
void
readseg(uchar* pa, uint count, uint offset)
{
  uchar* epa; // end phy addr

  epa = pa + count; // 结束地址

  // 这里将起始物理地址回退了offset的SECTSIZE(512)余数个Byte
  // 因为在从磁盘上读数据的时候,以512字节进行读取。所以offset会以512为单位向下取整,
  // 被换算成扇区号的偏移。如果offset原来不是SECTSIZE的整数倍,向下取整会导致offset截断,
  // 记换算后的offset为ofs,此时如果直接读取ofs扇区会多读offset % SECTSIZE个字节,
  // 所以需要提前将pa的地址减去offset % SECTSIZE个字节来排除offset被截断的多余的字节。
  // Round down to sector boundary.
  pa -= offset % SECTSIZE;    

  // 将offset换算成扇区号
  // Translate from bytes to sectors; kernel starts at sector 1.
  offset = (offset / SECTSIZE) + 1; 

  // 依次读取每个扇区,最后一个扇区多读了也没关系。
  for(; pa < epa; pa += SECTSIZE, offset++)
    readsect(pa, offset);
}

仔细推敲readseg函数中pa回滚的操作,总感觉这样贸然回滚,会覆盖之前已经被加载到内存中的代码。既然xv6敢这样写,八成说明是没有问题的,具体的原因,作者实在琢磨不透,如果有了解的朋友,可以在评论区交流一下。

跳到_start,正式进入内核前的预初始化

相关代码文件是entry.S

arm 复制代码
#include "asm.h"
#include "memlayout.h"
#include "mmu.h"
#include "param.h"

# Multiboot header.  Data to direct multiboot loader.
.p2align 2
.text
.globl multiboot_header
multiboot_header:
  #define magic 0x1badb002
  #define flags 0
  .long magic
  .long flags
  .long (-magic-flags)
  
# 这里就能看到,因为elf描述的一些标签是使用的虚拟地址,
# 而在进入entry开启分页前都是使用的物理地址,所以使用了
# V2P_WO宏将entry转换成了物理地址。方便bootmain跳转到entry.
# V2P_WO展开就是将输入的地址减去 0x8000 0000 的偏移。
.globl _start
_start = V2P_WO(entry)

# Entering xv6 on boot processor, with paging off.
.globl entry
entry:
  # 打开4M big page开关
  # Turn on page size extension for 4Mbyte pages
  movl    %cr4, %eax
  orl     $(CR4_PSE), %eax
  movl    %eax, %cr4

  # 将页目录设置为entrypgdir,同样由于在启用虚拟内存前,
  # 需要将虚拟地址entrypgdir转换为物理地址,有关entrypgdir
  # 的定义在内存管理章节进行详细叙述。
  # 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

  ########################### 以下正式进入分页模式,地址皆是虚拟地址
  # 再一次修改esp指针,将esp移到内核代码范围中
  # Set up the stack pointer.
  movl $(stack + KSTACKSIZE), %esp

  # 真正进入内核main函数,开始各种初始化。
  mov $main, %eax
  jmp *%eax

.comm stack, KSTACKSIZE

该部分代码负责进入main函数前的初始化,主要工作如下:

  1. 打开4M big page分页开关,让cpu支持4M大页。entrypgdir会将内核区域与映射为物理地址低4M的大页,我们后面会详细进行讨论,entrypgdir的生命周期非常短,在main函数中初始化过程中,会另外产生一个粒度更小的页表kpgdir(4K为一页),该页表会一直作为xv6的内核页表。

  2. 设置entrypgdir为BSP(booststrap processor)的页目录。

  3. 开启分页。

  4. 修改esp。指向内核自己分配的4K大小的栈上。

  5. 进入main。

在entry.S代码执行完后,cpu开启分页模式,所有的地址都将以虚拟地址的形式存在。此时内存布局如下:

至此,cpu的预初始化进行完毕。下面几章将围绕内核的初始化去讲解类unix操作系统的内存管理、进程调度、文件系统子模块。


本章完结

相关推荐
W Y15 分钟前
【架构-37】Spark和Flink
架构·flink·spark
半盏茶香25 分钟前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
小堇不是码农31 分钟前
在VScode中配置C_C++环境
c语言·c++·vscode
Jack黄从零学c++33 分钟前
C++ 的异常处理详解
c++·经验分享
Gemini199536 分钟前
分布式和微服务的区别
分布式·微服务·架构
捕鲸叉6 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer6 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq6 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
青花瓷8 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
Dann Hiroaki9 小时前
GPU架构概述
架构