ucore lab1 练习3—bootloader进入保护模式

练习3:分析bootloader进入保护模式的过程。(要求在报告中写出分析)

BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。

提示:需要阅读小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:

  • 为何开启A20,以及如何开启A20
  • 如何初始化GDT表
  • 如何使能和进入保护模式

实模式与保护模式

Intel 80386 cpu主要有两种运行模式:实模式和保护模式。

cpu刚启动时处于实模式,实模式主要为兼容之前的cpu,如8086,只有20根地址线,cpu位宽为16位。因为寄存器为16位,无法完整访问整个存储空间,因此实模式采用分段的方式访问物理内存。物理地址计算方法为:

物理地址 = 段寄存器值 * 16 + 偏移值

段寄存器如cs、ds、ss、es等,偏移值存储在通用寄存器中,通过两个寄存器按照上述方法来直接访问物理内存。程序可直接访问物理内存,并无保护功能。

同时,因为只有20根地址线,只能访问1M的物理内存空间,但是通过实模式的segment:offset寻址方式最大地址为0xffff << 4 + 0xffff = 0x10ffef,超出了20根地址线,在8086中会发生地址回卷,最高的第20位被丢掉(从0开始),实际物理内存地址为0x0ffef。随后的处理器,如80286、80386都拥有更多的地址线,为使得其在实模式下与8086表现保持一致,实模式下使得第20根地址线恒为0,发生回卷时,则物理地址不会超出1M的内存空间。因此80386提供了方式用于是否打开A20地址线,若cpu进入保护模式需打开A20。

80386保护模式下可以使用32位地址线,访问4GB的物理内存空间。保护模式下程序访问内存使用分段存储管理机制,内存可被划分为若干段,每一段拥有基址、界限、特权级、权限等属性,程序在访问段时可进行安全检查,保证安全,因此叫做保护模式,突出对内存保护的安全特性。

内存被分为多个段,如代码段、数据段、堆、栈等,每一段的属性为段描述符,多个段描述符组成段描述符表。段描述符表存储在物理内存中,30386中拥有段描述符表寄存器(GDTR、LDTR),用于存储段描述符表的基址和界限。描述符表分为全局段描述符表(GDT)与本地段描述符表(LDT),OS使用的为GDT与GDTR。

段描述符格式如下所示,共占8个字节:

GDTR全局描述符寄存器格式如下所示,分为32位基址与16位界限:

内存地址的概念有多种,如逻辑地址、线性地址、物理地址。

逻辑地址即程序中看到和使用的地址,由段选择器和偏移组成(segment:offset),此时段寄存器不再作为段地址了,而是段描述符表中的索引,用于选择一个段,从该段的描述符中得到段基址,与偏移值相加,得到线性地址。分段地址转换过程如下所示:

若再增加分页机制,线性地址经过转换后成为物理地址。若无分页机制,线性地址即为物理地址。

在ucore中,内存管理主要使用分页管理机制,而很少依赖分段管理机制。在配置段表时,不对逻辑地址做任何改变,也即逻辑地址=线性地址。bootloader中由于没有分页机制,因此逻辑地址=线性地址=物理地址。

BootLoader编写

为学习bootloader将cpu从实模式切换为保护模式的过程,根据bootasm.S代码编写了测试的bootloader,完成实模式到保护模式的过程,并且可以向显示器输出hello world等信息。相关代码位于boot_test文件夹。

初始化

.global start
start:
.code16     # 16位实模式
    cli     # 关闭中断
    cld     # 字符串操作时方向递增

    xorw %ax, %ax       # 通过异或操作,设置ax寄存器值为0
    # 设置ds, es, ss寄存器为0
    movw %ax, %ds      
    movw %ax, %es
    movw %ax, %ss 

bootloader入口地址为start,此时处于实模式。首先需要关闭中断,避免产生中断被BIOS中断处理程序处理。之后将各个段基址设为0。

开启A20

# 打开A20地址线
inb $0x92, %al      # 通过0x92端口打开A20
orb $0x2, %al
outb %al, $0x92

A20线可由多种方式启动,这里通过向位于0x92端口的Fast Gate A20的第1位置1来开启A20。

初始化GDT表

#define SEG_NULLASM                                             \
    .word 0, 0;                                                 \
    .byte 0, 0, 0, 0

#define SEG_ASM(type,base,lim)                                  \
    .word (((lim) >> 12) & 0xffff), ((base) & 0xffff);          \
    .byte (((base) >> 16) & 0xff), (0x90 | (type)),             \
        (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)


/* Application segment type bits */
#define STA_X       0x8     // Executable segment
#define STA_E       0x4     // Expand down (non-executable segments)
#define STA_C       0x4     // Conforming code segment (executable only)
#define STA_W       0x2     // Writeable (non-executable segments)
#define STA_R       0x2     // Readable (executable segments)
#define STA_A       0x1     // Accessed

asm.h文件通过宏的方式来定义了初始化段描述符的宏函数。该函数中,段描述符的G=1,段界限已4K为单位,但参数lim以字节为单位,因此在段界限分片时均右移12位(除以4K)。

.word分别定义了段界限150、段基址150

.byte分别定义了如下四部分:

  1. 段基址23~16

  2. P=1、DPL=00、S=1、TYPE=type

  3. G=1、D/B=1、L=0、AVL=0、段界限19~16

  4. 段基址31~24

# 设置GDT表
    lgdt gdtdesc        # 将gdt信息写入GDTR(包括基址与界限)

# bootloader的GDT表
.p2align 2      # 强制4字节对齐
gdt:
    SEG_NULLASM     # 第一个段为空段
    SEG_ASM(STA_X | STA_R, 0x0, 0xffffffff)     # 代码段
    SEG_ASM(STA_W, 0x0, 0xffffffff)     # 数据段


gdtdesc:
    .word 0x17      # GDT边界,三个段,共3 * 8 = 24 B,值为24 - 1 = 23 (0x17)
    .long gdt       # GTD基址,长度32

初始化GDT时,创建了3个段,第一个为空段,第二为代码段,第三个为数据段,代码段和数据段基址均为0,总长度都为整个内存空间4G大小,因此逻辑地址=线性地址。

通过lgdt指令来将GDT信息写入GDTR。

进入保护模式

.set PROT_MODE_CSEG, 0x8        # 代码段选择子  0000 1 000
.set PROT_MODE_DSEG, 0x10       # 数据段选择子  0001 0 000 
.set CR0_PE_ON, 0x1             # 保护模式打开标志

	# 切换为保护模式
    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0 

    ljmp $PROT_MODE_CSEG, $protcseg 

.code32
protcseg:
    # 设置段寄存器选择子为GDT表中的数据段
    movw $PROT_MODE_DSEG, %ax
    movw %ax, %ds 
    movw %ax, %es
    movw %ax, %fs
    movw %ax, %gs 
    movw %ax, %ss

    # 设置堆栈,从0开始到0x7c00(bootloader起始地址)
    movl $0x0, %ebp
    movl $start, %esp

通过将cr0寄存器的第0位(PE位:保护模式允许位)置1,cpu进入保护模式。通过ljmp指令跳转到32位代码开始执行,同时更新了代码段的选择子为0x8,以及清空了流水线过时的16位代码。

段选择子的结构如下所示:

在cpu段机制通过选择子查找GDT时,将会自动将index * 8 作为GDT的索引(字节为单位),因此位于第二个段的代码段选择子应为0x8,低3位为RPL。

进入保护模式后,将各个段寄存器设置为新的选择子,并设置堆栈指针从0x7c00出向下增长,用来后续执行C语言的函数。

输出Hello World!

print:
    # 向显卡输出字符
    movl $0xb8000, %eax
    movl $0x5a0, %ebx
    movb $'H', 0x0(%ebx, %eax)
    movb $'e', 0x2(%ebx, %eax)
    movb $'l', 0x4(%ebx, %eax)
    movb $'l', 0x6(%ebx, %eax)
    movb $'o', 0x8(%ebx, %eax)
    movb $',', 0xa(%ebx, %eax)
    movb $'W', 0xc(%ebx, %eax)
    movb $'o', 0xe(%ebx, %eax)
    movb $'r', 0x10(%ebx, %eax)
    movb $'l', 0x12(%ebx, %eax)
    movb $'d', 0x14(%ebx, %eax)
    movb $'!', 0x16(%ebx, %eax)

spin:
    jmp spin

0xB8000~0xBFFFF是显卡的地址空间,上段代码向该地址空间依次输出Hello World!

在 AT&T 汇编格式中,内存操作数的寻址方式是:

section:disp(base, index, scale)

段基址为0,因此计算出的线性地址为:disp + base + index * scale

运行结果

可见qemu中输出了Hello World!

参考

本项目github

posted @ 2020-07-16 11:02  whileskies  阅读(721)  评论(0编辑  收藏  举报