Lab1:Part 2 The Boot Loader

Lab1:Part 2 The Boot Loader

PC的软盘和硬盘分为多个512个字节大小的的区域,称为扇区。 扇区是磁盘的最小传输单位:每个读或写操作必须是一个或多个扇区,并且必须在扇区边界上开始。 如果磁盘是可引导的,则第一个扇区称为引导扇区,因为这是引导加载程序代码所在的位置。 当BIOS找到可引导的软盘或硬盘时,它将512字节的引导扇区加载到物理地址0x7c000x7dff的内存中,然后使用jmp指令将CS:IP设置为0000:7c00,将控制权传递给引导程序装载机。

对于6.828,我们将使用常规的硬盘启动机制。 引导加载程序由一个汇编语言源文件boot/boot.S和一个C源文件boot/main.c组成。浏览源文件,了解其具体做了什么。 引导加载程序必须执行两个主要功能:

  1. 首先,引导加载程序将处理器从实模式切换到32位保护模式,因为只有在这种模式下,软件才能访问1MB以上的所有内存。
  2. 其次,引导加载程序通过 x86 的特殊 I/O 指令直接访问 IDE 磁盘设备寄存器,从而从硬盘读取内核。

了解了引导加载程序的源代码之后,请查看文件 obj/boot/boot.asm ,这是引导加载程序的反汇编版本,该文件可以轻松地准确查看所有引导加载程序代码在物理内存中的位置,并且可以更轻松地跟踪在 GDB 中逐步引导加载程序时发生的情况,对于调试很有帮助。

GBD 指令

  1. b --- 在指定地址设置断点,如 b *0x7c00

  2. c --- 执行到下一个断点或直到按 Ctel C 为止

  3. si N --- 执行到后面的第 N 条

  4. x/i --- 检查内存中的指令(除即将执行的下一条指令外),如x/Ni addr 现实从 addr 开始的 N 条指令

boot 文件下 boot.S 与 mian.c 的源码阅读

boot.s

 cli                         # Disable interrupts 

cli 是 boot.S 的第一条指令,关全局中断。

 cld                         # String operations increment

cld 用于将 DF 位(Direction Flag) 置零,DF 用于串操作指令中决定内存地址的变化方向,DF 置零使得串操作朝地址增加方向。

 # Set up the important data segment registers (DS, ES, SS).
 xorw    %ax,%ax             # Segment number zero
 movw    %ax,%ds             # -> Data Segment
 movw    %ax,%es             # -> Extra Segment
 movw    %ax,%ss             # -> Stack Segment

通过 xorw 指令使得 ax 寄存器内容清零,在通过 movw 指令给 ds, es, ss 寄存器置零。因为经历了 BIOS 后,这三个寄存器存放的内容不确定,需要重置,为进入保护模式做准备。

  # Enable A20:
  #   For backwards compatibility with the earliest PCs, physical
  #   address line 20 is tied low, so that addresses higher than
  #   1MB wrap around to zero by default.  This code undoes this.
seta20.1:
  # 从 0x64 端口读取一个字节,存于 ax 寄存器的低八位
  inb     $0x64,%al               # Wait for not busy
  # 测试 al 中的第二位(与操作),若结果为 0 则 ZF = 1
  testb   $0x2,%al
  # ZF 为 0 就跳转循环 seta20.1
  jnz     seta20.1

  # 0x64 端口空闲,将 0xdl 送往该端口
  movb    $0xd1,%al               # 0xd1 -> port 0x64
  outb    %al,$0x64

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

  # 将 0xdf 送往 0x64 端口
  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60

http://bochs.sourceforge.net/techspec/PORTS.LST 中找到了 0x64 端口及其各位的作用:

该端口属于键盘控制器 804x,名称是控制器读取状态寄存器。

testb 0x02, %al 测试 0x64 端口的 bit 1, 若其为 1(该指令会将 ZF 标志置 1),则说明输入缓冲满,不能马上往该端口写入数据,需要重新测试端口(jnz seta20.1 指令),直到端口空闲。

上述代码分别给0x64 和 0x60 端口写入两条指令0xd1,0xdf,查到这两条指令的含义如下

0xd1 指令将下一条写到0x60端口的指令写到键盘控制器 804x 的输出端口,即0xdf被写到键盘控制器 804x 的输出端口。

0xdf 指令使能 A20 线,代表可以进入保护模式。

  # Switch from real to protected mode, using a bootstrap GDT
  # and segment translation that makes virtual addresses 
  # identical to their physical addresses, so that the 
  # effective memory map does not change during the switch.
  lgdt    gdtdesc
  # 修改寄存器 cr0 的 bit0,使其为 1
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax    # CR0_PE_ON 定义于 mmu.h 文件,内容为 0x00000001
  movl    %eax, %cr0

lgdt 指令用于加载全局描述符,用于将gdtdesc这个标识符的值送入全局映射描述符表寄存器 GDTR 中。gdtdesc是一个标识符,标识着一个内存地址。从这个内存地址开始之后的6个字节中存放着 GDT 表的长度和起始地址,GDT 表是处理器在保护模式下一个很重要的表。(我没弄清楚,参考 这儿

加载 LGDT 结束后,将 CR0 寄存器的 bit1 置 1。

# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
# gdt 表,共三个表项
gdt:
  SEG_NULL				# null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff)	# code seg
  SEG(STA_W, 0x0, 0xffffffff)	        # data seg

gdtdesc:
  # 表的大小
  .word   0x17                            # sizeof(gdt) - 1
  # 起始地址
  .long   gdt                             # address gdt

由于 xv6 其实并没有使用分段机制,也就是说数据段和代码段的位置没有区分,所以数据段和代码段的起始地址都是0x0,大小都是0xffffffff

  # Jump to next instruction, but in 32-bit code segment.
  # Switches processor into 32-bit mode.
  ljmp    $PROT_MODE_CSEG, $protcseg

该指令跳转至 protcseg,并将运行模式切换至 32 位地址模式

  .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

在加载完 GDTR 后必须重新加载上述所有寄存器的值,这是必须要求的:https://en.wikibooks.org/wiki/X86_Assembly/Global_Descriptor_Table

  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call bootmain

设置当前 esp 寄存器的值,然后跳转至 main.c 中的 bootmain 函数。

main.c

参考

MIT 6.828 JOS学习笔记5. Exercise 1.3

猜你喜欢

转载自www.cnblogs.com/joe-w/p/12554360.html