Xv6源代码之boot

一 基本原理

       计算机加电启动后,CPU一开始会执行称为BIOS(基本输入/输出系统)的程序,该程序存储在主板上的非易失性存储器(ROM)中。 BIOS的工作是准备硬件,然后将控制权交给操作系统。具体来说,计算机系统的 BIOS 在完成一系列的初始化后会选择一个启动设备(例如硬盘、光盘、软盘等),并且读取该设备的第一扇区(磁盘最开始的 512 个字节),如果发现它以 0xaa55 结束,则 BIOS 认为它是一个引导扇区,将其装载到指定的内存位置(CS 设置为 0x0000,IP设置为 0x7c00,即物理内存 0x07c00)内存中,然后跳转到 0x07c00 处将控制权彻底交给这段引导代码。到此,计算机不再由 BIOS 中固有的程序来控制,而是变成由操作系统的一部分来控制。当引导加载程序开始执行时,处理器模拟Intel 8088,加载程序的工作是将处理器置于更现代的操作模式,将xv6内核从磁盘加载到内存中,然后将控制权交给内核。 xv6引导加载器包含两个源文件,一个是以16位和32位x86汇编(bootasm.S)的组合编写的,一个是C(bootmain.c)。

二 实模式与保护模式

       自从 1969 年推出第一个微处理器以来, Intel 处理器就在不断地更新换代,从 8086、 8088、80286,到 80386、80486、奔腾、奔腾 II、奔腾 4 等,其体系结构也在不断变化。从 80386开始,增加了一些新的功能,弥补了 8086 的缺陷。这其中包括内存保护、多任务以及使用640KB 以上的内存等,但仍然保持和 8086 家族的兼容性。也就是说 80386 仍然具备了 8086和 80286 的所有功能,但是在功能上有了很大的增强。早期的处理器工作在实地址模式
RM(Real-address Mode)之下的,80286 以后引入了保护模式 PM(Protected-address Mode),而在 80386 以后保护模式又进行了很大的改进。在 80386 中,保护模式为程序员提供了更好的保护,提供了更多的内存。

      早期的PC是基于16位的Intel 8088的处理器的,其操作模式称为“实地址模式”,只支持1MB的物理内存,因此早期的PC的物理内存是从0x00000000到0x000FFFFF,而不是结束于0xFFFFFFFF。物理内存的前640KB被标记为了“Low Memory”,这一块区域是早期的PC 唯一可以使用的 RAM。而从 0x000A0000 到0x000FFFFF 的 384KB 的区域是被硬件保留着用于特殊用途的,比如像作为 VGA 的显示输出的缓存或者是被当作保护系统固化指令的非易失性存储器。这一部分内存区域中最重要的应该是保存在 0x000F0000 到 0x001000000 处占据 64KB 的 BIOS(基本输入输出系统)。

    在 RM 模式下,对内存地址的访问是通过 Segment:Offset 的方式来进行的,其中 Segment是一个段的 BaseAddress(Segment 的最大长度是 64KB,这是 16-bit 系统所能表示的最大长度)。而 Offset 则是相对于此 Segment Base Address 的偏移量。 BaseAddress +Offset 就是一个内存绝对地址。由此可以看出,一个段具备两个因素: BaseAddress 和 Limit(段的最大长度)。对一个内存地址的访问需要指出:使用哪个段?以及相对于这个段 BaseAddress 的 Offset,这个 Offset 应该小于此段的 Limit(对于 16-bit 系统,Limit 默认为最大长度 64KB,而 16-bit的 Offset 也永远不可能大于此 Limit)。在实际编程时,使用 16-bit 段寄存器 CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定 Segment,CPU 将段寄存器中的数值向左偏移4-bit,放到 20-bit 的地址线上就成为 20-bit 的 Base Address。   

       在 PM 模式下,内存的管理模式分为两种,段模式和页模式。其中段模式是必不可少的,页模式是可选的,且是基于段模式的——如果使用页模式,则是段页式。因此 PM 模式下的内存管理模式事实上是:纯段模式和段页式。

  (1) 分段存储管理机制

        这里暂且只考虑段模式,访问一个内存地址自然使用 Segment:Offset 的方式。由于 PM模式运行在 32-bit 系统上,那么 Segment 的两个因素:BaseAddress 和 Limit 也都是 32 位的。IA-32 允许将一个段的 BaseAddress 设为 32-bit 所能表示的任何值。另外,PM 又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在 PM 模式下,对一个段的描述包括 3 方面因素:[BaseAddress,Limit,Access],它们加在一起被放在一个 64-bit 长的数据结构中,被称为段描述符,其结构如图 2-1 所示。通常,将这些 64-bit 的段描述符放入一个全局数组中,称之为全局描述符表(GDT)。 GDT 可以被放在内存的任何位置,为了跟踪 GDT,处理器内部有一个 48 位的寄存器,称为全局描述符寄存器(GDTR),其结构如图 2-2 所示。该寄存器分为两部分,分别是 32 位的线性地址和 16 位的边界。为了引用 GDT 的段描述符所描述的段,需要通过存放在段寄存器中的 16-bit 结构作为下标间接应用。这个数据结构叫做 SegmentSelector——段选择子,如图 2-3 所示。

                                                               图 2-1 段描述符结构

                                                                  图 2-2 GDTR 结构

                                                                 图 2-3 段选择子结构

        例如,系统的“段选择子”为:0x0008,对应二进制串 0000 0000 0000 1000,指定了GDT 中具有 RPL=0 的段 1,其索引字段值为 1。逻辑地址 0x0008:0x0000 如何转换为线性地址呢?在 PM 模式下,当需要引用一个内存地址时,使用的仍然是 Segment:Offset 模式,具体操作是:在相应的段寄存器装入 SegmentSelector,按照这个 SegmentSelector 可以到 GDT 或LDT 中找到相应的 SegmentDescriptor,这个 SegmentDescriptor 中记录了此段的 BaseAddress,然后加上 Offset,就得到了最后的内存地址。如图 2-4 所示。

                                                                      图 2-4 地址转换

(2) A20 地址线

       在8086、8088中,有20根地址线,所以寻址范围是1M,但8086/8088是16位的地址模式,即只能表示64K的范围。为了能访问1M的内存采取了分段的模式。16位段基址:16位偏移,达到了0x10FFEF,但是8086/8088的内存不可能超过1MB,所以当时的程序超过1MB时会自动回卷。但是到了80386地址线达到32根,芯片也达到32-bit,寻址能力达到4GB,但是为了向后兼容,IBM采用了一个控制方法,用键盘控制器上的一个剩余的控制线来控制,即A20控制线。

 

        A20 对应于地址中的第 20-bit(从 0 开始数)的特殊处理(也就是对第 21 根地址线的处理)。对于 80386 极其随后的 32-bit 芯片来说,如果 A20Gate 被禁止,则其第 20-bit 在 CPU 做地址访问的时候是无效的,永远只能被作为 0;如果 A20Gate 被打开,则其第 20-bit 是有效的,其值既可以是 0,又可以是 1。

                                                              图2-5 A20 Gate
        所以,在保护模式下,如果 A20Gate 被禁止,则可以访问的内存只能是奇数 1M 段,即1M,3M,5M...,也就是 00000-FFFFF,200000-2FFFFF,300000-3FFFFF...。如果 A20Gate 被打开,则可以访问的内存则是连续的。

                                                                 图 2-6 地址空间
        多数 PC 都使用键盘控制器(8042 芯片)来处理 A20Gate。

(3) CR0 控制寄存器

        80386 有 4 个 32 位的控制寄存器,名字分别为 CR0、CR1、CR2 和 CR3。这些寄存器仅能够由系统程序通过 MOV 指令访问。其中 CR0 格式如图 2-7 所示。

                                                                    图 2-7CR0 寄存器
        控制寄存器 CR0 包含系统整体的控制标志,它控制或指示出整个系统的运行状态或条件。其中 PE 位为保护模式开启位(Protection Enable,第 0 位),如果设置了该位,就会使处理器开始在保护模式下运行。
        系统切换到保护模式,实际就是把 PE 位设置为 1。当然为了把系统切换到保护模式,还要做一些其他的事情。启动程序必须要对系统的段寄存器和控制寄存器进行初始化。把PE 位设置为 1 以后,还要执行跳转指令。过程简述如下。
Step 1: 创建 GDT 表。
Step 2: 用 lgdt 命令加载 gdtr。
Step 3: 启用 A20 地址线。
Step 4: 通过置 CR0 的 PE 位为 1。
Step 5: 执行跳转,进入保护模式。

bootasm.S:

#include "asm.h"
#include "memlayout.h"
#include "mmu.h"

# Start the first CPU: switch to 32-bit protected mode, jump into C.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                         # 关中断; 

  # 寄存器置为0
  xorw    %ax,%ax             # Set %ax to zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

  # 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.
  #打开A20Gate
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1


  movb    $0xd1,%al               # 0xd1 -> port 0x64  把0xd1这条数据写入到0x64端口 理解为下一个写入0x60端口的数据是一个控制指令

  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> port 0x60 0Xdf指令含义就是使能A20线
  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  #利用lgdt指令加载GDTR寄存器 gdtdesc是一个标识符,标识着一个内存地址
  movl    %cr0, %eax #设置CR0
  orl     $CR0_PE, %eax
  movl    %eax, %cr0

//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
  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 设置了临时的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    

bootmain.c

           bootmain.c目的是在磁盘的第二个扇区开头找到内核程序并加载到内存中。
// Boot loader.
//
// Part of the boot block, along with bootasm.S, which calls bootmain().
// bootasm.S has put the processor into protected 32-bit mode.
// bootmain() loads an ELF kernel image from the disk starting at
// sector 1 and then jumps to the kernel entry routine.
//内核二进制文件是ELF格式的,所以bootmain通过ELF文件格式可以得到内核的程序入口
#include "types.h"
#include "elf.h"
#include "x86.h"
#include "memlayout.h"

#define SECTSIZE  512

void readseg(uchar*, uint, uint); //从硬盘中读取数据

void
bootmain(void)
{
  struct elfhdr *elf;
  struct proghdr *ph, *eph;
  void (*entry)(void);
  uchar* pa;

  elf = (struct elfhdr*)0x10000;  // scratch space临时空间

  // Read 1st page off disk从磁盘读取第一页的内容到ELFHEAD(0x10000)处 相当于把操作系统映像文件的elf头部读取出来放到内存中
  readseg((uchar*)elf, 4096, 0);

  // Is this an ELF executable? 对elf头部信息进行验证 elf头部信息的Magic字段是整个头部信息的开端。
  if(elf->magic != ELF_MAGIC)

    return;  // let bootasm.S handle error

elf文件:elf是一种文件格式,主要被用来把程序存放到磁盘上。一个elf文件包括多个段,elf文件的头部就是用来描述这个elf文件如何在存储器中存储。




  // Load each program segment (ignores ph flags).
  ph = (struct proghdr*)((uchar*)elf + elf->phoff); //Program Header Table 其中存放着程序中所有段的信息
  eph = ph + elf->phnum;
  for(; ph < eph; ph++){ //把内核中的各个段从外存读入内存中
    pa = (uchar*)ph->paddr;
    readseg(pa, ph->filesz, ph->off);
    if(ph->memsz > ph->filesz)
      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); //跳转到操作系统内核程序的起始指令处
  entry();
}

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


// Read a single sector at offset into dst.
//读取第offset块磁盘到dst
void
readsect(void *dst, uint offset)
{
  // Issue command.
  waitdisk();
  outb(0x1F2, 1);   // count = 1
  outb(0x1F3, offset);
  outb(0x1F4, offset >> 8);
  outb(0x1F5, offset >> 16);
  outb(0x1F6, (offset >> 24) | 0xE0);
  outb(0x1F7, 0x20);  // cmd 0x20 - read sectors

  // Read data.
  waitdisk();
  insl(0x1F0, dst, SECTSIZE/4);

}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked.
//从磁盘中读取数据的函数,从硬盘中读取数据到pa开始的内存地址,count是字节数目,offset是开始的字节数。
void
readseg(uchar* pa, uint count, uint offset)
{
  uchar* epa;
  epa = pa + count;
// Round down to sector boundary.
//扇区是磁盘可以寻址的最小单位

  pa -= offset % SECTSIZE;

  // Translate from bytes to sectors; kernel starts at sector 1.
  offset = (offset / SECTSIZE) + 1;

  // If this is too slow, we could read lots of sectors at a time.
  // We'd write more to memory than asked, but it doesn't matter --
  // we load in increasing order.
  for(; pa < epa; pa += SECTSIZE, offset++)
    readsect(pa, offset);

 

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值