实验内容
PC机的软盘和硬盘被划分为固定的512字节的扇区(sector)。扇区是磁盘的最小传输粒度:每个读或写操作的大小必须是一个或多个扇区。第一个扇区称为the boot sector引导扇区,从引导扇区加载操作系统。当BIOS找到可以启动的软盘或硬盘之后,这个512个字节的启动扇区就会被加载到内存,地址为0x7c00~0x7dff区域。
bootloader程序的大小必须是小于512个字节的。bootloader由一个汇编文件boot/boot.S以及一个C语言文件boot/main.c组成。bootloader要完成以下主要功能:
1. bootloader要把处理器从实模式转换为保护模式,因为只要在保护模式下软件才可以访问超过1MB空间的内容。
2. boot loader可以通过使用x86的特定的IO指令,直接访问IDE磁盘设备寄存器,从磁盘中读取内核。(这个功能还没搞懂,标记一下)
Exercise 3
启动gdb调试。设置断点在0x7c00处,执行c,可以看到QEMU终端显示Boot from hard disk.
执行x/30i 0x7c00可以看到一段汇编语言。 这条gdb指令是把存放在0x7c00以及之后30字节的内存里面的指令反汇编出来。
(gdb) b *0x7c00
Breakpoint 1 at 0x7c00
(gdb) c
Continuing.
The target architecture is assumed to be i8086
[ 0:7c00] => 0x7c00: cli
Breakpoint 1, 0x00007c00 in ?? ()
(gdb) x/30i 0x7c00
=> 0x7c00: cli
0x7c01: cld
0x7c02: xor %ax,%ax
0x7c04: mov %ax,%ds
0x7c06: mov %ax,%es
0x7c08: mov %ax,%ss
0x7c0a: in $0x64,%al
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
0x7c10: mov $0xd1,%al
0x7c12: out %al,$0x64
0x7c14: in $0x64,%al
0x7c16: test $0x2,%al
0x7c18: jne 0x7c14
0x7c1a: mov $0xdf,%al
0x7c1c: out %al,$0x60
0x7c1e: lgdtw 0x7c64
0x7c23: mov %cr0,%eax
0x7c26: or $0x1,%eax
0x7c2a: mov %eax,%cr0
0x7c2d: ljmp $0x8,$0x7c32
0x7c32: mov $0xd88e0010,%eax
0x7c38: mov %ax,%es
0x7c3a: mov %ax,%fs
0x7c3c: mov %ax,%gs
0x7c3e: mov %ax,%ss
另外,同样在0x7c00处设置断点,使用vim打开boot/boot.S,并与反汇编指令进行比较。
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
可以看到,cli是bootloader执行的第一条指令。作用是关闭所有中断。
cld: clear direction flag, 指定之后发生的串处理操作的指针移动方向。(啥意思没搞懂 mark一下)
# 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
以上四条指令是把三个段寄存器, Data Segment, Extra Segment, Stack Segment全部清零,为进入保护模式做准备。
# 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:
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
以上指令的作用是准备把PC的工作模式从实模式转换为保护模式。使能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
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
gdtdesc是一个标识符表示一个内存地址。从这个内存地址开始,之后的6个字节存放着GDT表的长度和起始地址。
接着的三条指令是修改CR0寄存器的内容。把CR0寄存器的bit0置1,CR0寄存器的bit0是保护模式启动位,把这位置0之后表示保护模式启动。
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
当前的运行模式切换成了32位地址模式。
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain
设置esp寄存器的值,准备跳转到main.c文件中bootmain函数。
接下来,关闭boot.S文件,用vim指令打开boot/main.c文件。并且追踪到bootmain()函数。
bootmain()函数中首先调用的是函数readseg,根据注释理解它的功能是:把距离内核地址offset个偏移量存储单元作为起始地址,把它和它之后的count字节的数据读出送入以pa为起始地址的内存地址。实际上这就是把内核的第一个页的内容的内容读取的内存地址ELFHDR(0x10000)处。其实就是把操作系统映像文件的elf头部读取放入内存中。
[注] elf是一种文件格式,主要被用来把程序存放到磁盘上。是在程序被编译和链接后被创建出来的。一个elf文件包括多个段。对于一个可执行程序,通常包含存放代码的文本段(text section),存放全局变量的data段,存放字符串常量的rodata段。elf文件的头部就是用来描述这个elf文件如何在存储器中存储。
// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)