Lab 1
Part 1:PC Bootstrap
1、预备知识:
- BIOS:
- 第一代PC处理器是16位字长的Intel 8088处理器,这类处理器只能访问1MB的地址空间,即0x00000000-0x000FFFFF。这1MB中0x000F0000——0x00100000被只读内存(ROM)所使用,计算机通电后,第一件事就是读取它。ROM里的程序叫做"基本輸出輸入系統"(Basic Input/Output System),简称为BIOS。
- SeaBIOS是qemu和kvm的默认BIOS 。在SeaBIOS中会完成虚拟硬件的初始化,中断服务函数的设置,ACPI表、SMBIOS表、E820表等的创建,最后引导启动OS。
- 实模式与保护模式:
- 实模式是早期CPU,比如8088处理器的工作模式,这类处理器由于只有20根地址线,所以它们只能访问1MB的内存空间。实模式下的地址由段基址(segment)和偏移(offset)两部分组成,计算公式为:segment * 16 + offset。
- 但是CPU也在不断的发展,之后的80286/80386已经具备32位地址总线,能够访问4GB内存空间,为了能够很好的管理这么大的内存空间,保护模式被研发出来。在保护模式下,虽然段值仍然由原来的16位cs寄存器指定,但此时这些寄存器中存放的不再是段基址,而是一个索引。从这个索引,可以找到一个表项,里面存放了段基址等很多属性,这个表项称为段描述符,这个表就称为GDT。
- 现代处理器都是工作在保护模式下的。但是为了实现向后兼容性,即原来运行在8088处理器上的软件仍旧能在现代处理器上运行,现代的CPU都是在启动时运行于实模式,启动完成后运行于保护模式。
- BIOS就是PC刚启动时运行的软件,所以它必然工作在实模式下。
2、利用GDB跟踪ROM BIOS的指令
首先进入lab所在文件夹,分别在两个终端先后输入如下命令:
Make qemu-nox-gdb;
Make gdb;
执行的BIOS的第一条指令如下:
这条指令的地址是0xffff0,而最前面的[ f000:fff0 ]源于实模式下的取址方式:0xffff0=0xf0000<<4+0xfff0
在gdb的终端输入si
得到下一条指令:
输入i r esi cs
查看esi cs的值:
所以这条指令意思是将0xffc8和0xf0000地址处的值进行比较。
下一条指令是跳转指令:
如果ZF标志位为0的时候跳转,即上一条指令cmpw的结果不是0时跳转。
继续输入si
,得到下一条指令地址是0xfe066:
可见上面的跳转指令并没有跳转。这条指令的功能是把edx寄存器清零。
接下来的几条指令对部分寄存器做了一些处理:
[f000:e068] 0xfe068: mov %edx,%ss
[f000:e06a] 0xfe06a: mov $0x7000,%sp
[f000:e070] 0xfe070: mov $0x2d4e,%dx
[f000:e076] 0xfe076: jmp 0x5575ff02
[f000:ff00] 0xfff00: cli
最后一句cli表示clear Interrupt,作用是将EFLAGS标志寄存器的IF置为0。
EFLAGS标志寄存器有32位,不同位代表不同标志,如下图所示:
其中IF表示中断使能标志。
关于中断:中断对于CPU来说,分为外部中断和内部中断。将 IF标志设置为0,屏蔽的是来自硬件的可屏蔽中断请求。在 IF为0期间,只有CPU外部不可屏蔽中断(NMI)引脚上发来的硬件中断请求能得到响应,其它可屏蔽请求不被响应。
启动时的操作是比较关键的,所以肯定是不能被中断的,因此需要这个关中断指令用于关闭那些可以屏蔽的中断。
查看eflags寄存器:
意思是此时PF ZF位为1,其他位为0,验证了此时IF标志位为0。
接下来几条指令如下所示:
0xfff01: cld
0xfff02: mov %ax,%cx
0xfff05: mov $0x8f,%ax
0xfff0b: out %al,$0x70
0xfff0d: in $0x71,%al
0xfff0f: in $0x92,%al
0xfff11: or $0x2,%al
0xfff13: out %al,$0x92
0xfff15: mov %cx,%ax
便于理解,找到seaBios的源代码相应处:
transition32:
// Disable irqs (and clear direction flag)
cli
cld
// Disable nmi
movl %eax, %ecx
movl $CMOS_RESET_CODE|NMI_DISABLE_BIT, %eax
outb %al, $PORT_CMOS_INDEX
inb $PORT_CMOS_DATA, %al
// enable a20
inb $PORT_A20, %al
orb $A20_ENABLE_BIT, %al
outb %al, $PORT_A20
movl %ecx, %eax
根据注释,这些指令是为了准备进入32位保护模式:关闭了NMI中断,将A20位即第21个地址线使能。
接下来的源代码是:
transition32_nmi_off:
// Set segment descriptors
lidtw %cs:pmode_IDT_info
lgdtw %cs:rombios32_gdt_48
// Enable protected mode
movl %cr0, %ecx
andl $~(CR0_PG|CR0_CD|CR0_NW), %ecx
orl $CR0_PE, %ecx
movl %ecx, %cr0
关注lidt和lgdt两条指令:
- LGDT或LIDT的作用是将以源操作数位地址的6 字节的值加载到全局描述符表格寄存器 (GDTR) 或中断描述符表格寄存器 (IDTR)。这6个字节包含全局描述符表格 (GDT) 或中断描述符表格 (IDT) 的基址(线性地址)与限制(表格大小,以字节计)。 2 个低位字节代表限制,4 个高位字节代表基址。
- GDTR或IDTR寄存器的作用是在相应的全局描述符表格 (GDT) 或中断描述符表格 (IDT) 中进行定位。中断向量表:每一种中断都有自己对应的中断处理程序,这个中断的处理程序的首地址就叫做这个中断的中断向量。IDT表存放所有中断向量的。全局描述符表:与保护模式下32位寻址有关。
lidt这条指令在gdb中对应的是:
再次查看esi和cs的值:
所以这条指令的意思是把从0xf0000为起始地址处的6个字节的值加载到全局描述符表格寄存器IDTR中,查看这6个字节:
所以加载到全局描述符表寄存器(GDTR)的值是0x83c01901243c,并且GDT基址为0x83c01901。但是实模式下能访问的最大虚拟/物理地址为0xfffff,所以这个基址似乎是不对的。
接下来两条汇编指令:
0xfff2e: or $0x1,%cx
0xfff32: mov %ecx,%cr0
计算机中包含CR0~CR3四个控制寄存器,用来控制和确定处理器的操作模式。CR0寄存器的0bit是保护位,当该位被置1,代表开启了保护模式。这个操作明显是要把CR0寄存器的最低位(0bit)置1。
所以目前BIOS干了什么?
关闭中断、加载IDTR/GDTR、开启保护模式……
之后的过程比较复杂, BIOS完成一系列硬件,再进入到Boot phase找到可引导设备,并把操作系统的boot loader加载到内存中开始取址执行。
Part2:the Boot Loader
1、预备知识:
- 上一部分最后提到BIOS会找到可引导设备,具体地说,BIOS在运行的最后会去检测可以从当前系统的哪个设备中找到操作系统,通常来说是磁盘,也有可能是U盘等等。当BIOS确定了操作系统位于磁盘中,它就会把这个磁盘的第一个扇区,通常把它叫做启动区(boot sector)先加载到内存地址0x7c00~0x7dff中,这个启动区中包括一个非常重要的程序–boot loader,它会负责完成将整个操作系统从磁盘导入内存的工作,以及一些其他的非常重要的配置工作。在这之后操作系统才会开始运行。
2、跟踪 /boot/boot.S文件
由boot.s的开头说明可知这就是boot loader程序。
输入下面指令在地址0x7c00处设置断点,并让程序继续运行到这个断点:
b *0x7c00
c
终端显示:
说明bootloader的第一条指令是cli——关闭中断。
接下来设置了一些重要的数据段寄存器:
cld
xorw %ax,%ax # Segment number zero
movw %ax,%ds # -> Data Segment
movw %ax,%es # -> Extra Segment
movw %ax,%ss # -> Stack Segment
根据注释,下面又是熟悉的enable A20操作。
为什么又要enable A20?
进入Bootloader之前,有的BIOS可能会在短时间内切换到保护模式,这将允许其使用受保护模式的一些优点(例如32位作为默认地址大小),但是在进入bootloader程序之前会切换回来。
enable A20之后gdb显示下一条指令是:
意思是把从(%esi)地址起始的6个字节加载到gdtr寄存器中,但是查看esi的值却是0。
而在boot.asm这个反汇编文件,对应的指令是:
看起来和gdb显示的一样,只是下面接了一条奇怪的指令 fs jl
。
回到boot.S中,这里的汇编代码是 lgdt gdtdesc
,在boot.S最后定义了要加载到gdtr寄存器中的gdtdesc和全局描述符表gdt:
gdtdesc由一个word(2字节,值为0x17,代表gdt表的大小)和一个long(4字节,值为gdt的起始地址,代表gdt表的基址)组成,正好6字节。
而GDT表的初始化用到了SEG这个宏定义,在mmu.h定义如下:
所以GDT表中的一项就有8字节,64位。
所以实际执行的绝不是lgdtl (%esi)
,错误原因可能出在反汇编时,所以直接查看该处的机器码:
0f 01是操作码lgdtl,16是指16位模式,所以gdtdesc的地址由后面两个字节组成:64 7c。又因为小端法,所以地址是0x7c64。在内存中查看该地址的6个字节:
前2个字节是0x0017,对应.word 0x17
,后面四个字节组成的0x7c4c则是gdt表基址。根据前面分析,gdt表一项就有8个字节,而初始化时有3项,所以查看0x7c4c为起始的后24个字节:
关于gdt表的几个问题:
-
这些数字要如何理解呢?
1个gdt表项的64位有不同的含义,如下图所示:
在gdt表中的三个表项,分别代表三个段:null seg,