bootloader保护模式源码分析
以清华大学ucore操作系统学习课程作为学习目标,其学习资料可以参考:
清华大学操作系统课程 https://github.com/chyyuu/os_course_info
清华大学计算机系OS课程主站 http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring/
uCore OS实验指导书和源码网址 (2019) https://chyyuu.gitbooks.io/ucore_os_docs/
学堂在线原理和实验课视频 http://www.xuetangx.com/courses/course-v1:TsinghuaX+30240243X+sp/info
实验源码与答案 https://github.com/chyyuu/ucore_os_lab
环境准备
具体来说,在ubuntu系统中使用qemu来作为x86模拟运行器,加载ucore操作系统特定版代码来分析操作系统启动过程。
如果使用Windows环境,可以通过VirtualBox来安装Ubuntu虚拟机来运行模拟环境,也可以直接安装Ubuntu操作系统。
安装VirtualBox 虚拟机软件
https://www.virtualbox.org/wiki/Downloads
安装Ubuntu镜像
https://pan.baidu.com/s/11zjRK
已经制作好一个运行环境Ubuntu镜像,可以直接下载在VirtualBox中运行。
启动Ubuntu系统,默认用户名为moocos,root用户密码为一个空格。
课程安排设计8个实验,其中实验1主要是系统软件启动过程,实验1源码位置
项目组成结构
主要包含boot,kern,libs,tools等。
boot boot主要实现bootloader
boot/bootasm.S : 定义并实现了bootloader最先执行的函数start, 此函数进行了一定的初始化, 完成了从实模式到保护模式的转换, 并调用bootmain.c中的bootmain函数。
boot/bootmain.c: 定义并实现了bootmain函数实现了通过屏幕、 串口和并口显示字符串。 bootmain函数加载ucore操作系统到内存, 然后跳转到ucore的入口处执行。
boot/asm.h: 是bootasm.S汇编文件所需要的头文件, 主要是一些与X86保护模式的段访问方式相关的宏定义。
kern kern主要是关于操作系统核心代码实现
init是系统初始化部分,mm为内存管理部分,driver为外设驱动部分,trap为中断处理部分,debug为内核调试部分。
系统启动过程
![](https://i-blog.csdnimg.cn/blog_migrate/b46b831022fd04a8c0af28be95741a5d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e593aae6533426f1dd6a14b1165247eb.png)
系统加电,CPU从物理地址0xFFFFFFF0( 由初始化的CS: EIP确定, 此时CS和IP的值分别是0xF000和0xFFF0)) 开始执行。 在0xFFFFFFF0这里只是存放了一条跳转指令, 通过跳转指令跳到BIOS例行程序起始点。 BIOS做完计算机硬件自检和初始化后, 会选择一个启动设备( 例如软盘、 硬盘、 光盘等) , 并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处, 然后CPU控制权会转移到那个地址继续执行。
BIOS启动过程
BIOS是被固化在计算机ROM( 只读存储器) 芯片上的一个特殊的软件, 为上层软件提供最底层的、 最直接的硬件控制与支持。
PC加电后, CS寄存器初始化为0xF000, IP寄存器初始化为0xFFF0, 所以CPU要执行的第一条指令的地址为CS:IP=0xF000:0XFFF0( Segment:Offset表示) =0xFFFF0( Linear表示) 。 这个地址位于被固化EPROM中, 指令是一个长跳转指令 JMP F000:E05B 。 这样就开启了BIOS的执行过程。初始化硬件设备、 建立系统的内存空间映射图, 从而将系统的软硬件环境带到一个合适的状态, 以便为最终调用操作系统内核准备好正确的环境。 最终引导加载程序把操作系统内核映像加载到RAM中, 并将系统控制权传递给它。
bootloader启动过程
BIOS将通过读取硬盘主引导扇区到内存, 并转跳到对应内存中的位置执行bootloader,bootloader主要功能:
切换到保护模式,启用分段机制;读磁盘中ELF执行文件格式的ucore操作系统到内存;显示字符串;把控制权交给ucore操作系统;
qemu和gdb调试ucore
从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
修改 lab1/tools/gdbinit,内容为
set architecture i8086
target remote :1234
在 lab1目录下,执行 make debug,在看到gdb的调试界面(gdb)后,在gdb调试界面下执行如下命令si ,即可单步跟踪BIOS。
在gdb界面下,可通过如下命令来看BIOS的代码
```
x /2i $pc //显示当前eip处的汇编指令
```
改写Makefile文件
debug: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
在调用qemu时增加`-d in_asm -D q.log`参数,便可以将运行的汇编指令保存在q.log中。
由q.log中汇编指令可知,CPU上电,从0xfff0处开始执行,此位置时一条长跳转指令,跳转到0xe05b。
在bootloader初始化位置0x7c00 设置实地址断点,测试断点正常。
在tools/gdbinit结尾加上
set architecture i8086 //设置当前调试的CPU是8086
b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址,可看boot/bootasm.S的start地址处
c //continue简称,表示继续执行
x /2i $pc //显示当前eip处的汇编指令
set architecture i386 //设置当前调试的CPU是80386
运行 "make debug" 便可得到
可知从0x7c00处开始执行bootloader代码,即开始执行boot/bootasm.S中汇编代码。
bootloader 进入保护模式过程
代码执行到0x7c00处,进行以下操作
1.清理环境:将flag置0和将段寄存器置0
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# 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
CLI指令作用
用于禁止中断,具体来说使eflags中IF标志位=0,禁止响应可屏蔽硬件中断。CLI禁止中断发生,STL允许中断发生,这两个指令只能在内核模式下执行,不可以在用户模式下执行;
CLD指令作用
在计算机中,大部分数据存放在主存 中,8086CPU提供了一组处理主存中连续存放的数据串的指令——串操作指令。串操作指令中,源操作数用寄存器SI寻址,默认在数据段DS中,但允许段 超越;目的操作数用寄存器DI寻址,默认在附加段ES中,不允许段超越。每执行一次串操作指令,作为源地址指针的SI和作为目的地址指针的DI将自动修 改:+/-1(对于字节串)或+/-2(对于字串)。地址指针时增加还是减少取决于方向标志DF。在系统初始化后或者执行指令CLD指令后,DF=0,此时地址指针增1或2;在执行指令STD后,DF=1,此时地址指针减1或2。
2.开启A20:通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,可以访问4G的内存空间;
关于A20 Gate http://hengch.blog.163.com/blog/static/107800672009013104623747/
关于A20 Gate详细介绍可以参考 https://blog.csdn.net/u014106644/article/details/97002252/
# 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(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
3.初始化GDT表:一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可;
lgdt gdtdesc
通过lgdt汇编指令可以把GDTR描述符表的大小和起始位置存入gdtr寄存器中,指令格式如下:
lgdt [描述段描述符表的地址]
GDTR寄存器保存了GDT的32位基地址和16位表界限,基地址指GDT的0字节的线性地址,表界限指表中的字节个数,LGDT和SGDT指令用来分别装载和保存GDTR寄存器。对于保护模式的操作,作为处理器初始化过程一部分,一个新基地址必须装入GDTR。
全局描述符表及其定义如下所示,一共定义了三个段,第一个为空段,第二个为代码段,第三个为数据段。
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
4.进入保护模式:通过将cr0寄存器PE位置1便开启了保护模式;
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
其中CR0_PE_ON定义如下
.set CR0_PE_ON, 0x1 # protected mode enable flag
CR0为控制寄存器,其中第0位PE控制处理器启用保护模式
当PE=1时,处理器处于保护模式,启用段机制;PG为分页机制启用位,当PE=1,并且PG=1时,此时处理器启用分段,分页模式,逻辑地址到物理地址的转换需要经过段处理以及页处理。
5.通过长跳转更新cs的基地址;
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
将kernel代码段的基地址更新到CS寄存器
6.设置段寄存器,并建立堆栈;
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
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
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
将kernel数据段基地址更新到数据段寄存器DS,ES,SS中。ebp指向栈顶,esp指向栈底。
7.转到保护模式完成,进入boot主方法;
call bootmain
bootmain方法则从硬盘开始加载操作系统文件。
bootasm.S完整汇编代码如下:
#include <asm.h>
# Start the 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.
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# 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
# 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(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to 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
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.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
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
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 for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
参考资料
清华大学操作系统课程 https://github.com/chyyuu/os_course_info
清华大学计算机系OS课程主站 http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring/
uCore OS实验指导书和源码网址 (2019) https://chyyuu.gitbooks.io/ucore_os_docs/
学堂在线原理和实验课视频 http://www.xuetangx.com/courses/course-v1:TsinghuaX+30240243X+sp/info