zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    通用权限管理设计 之 数据库结构设计
    jQuery LigerUI 插件介绍及使用之ligerDateEditor
    jQuery LigerUI 插件介绍及使用之ligerTree
    jQuery LigerUI V1.01(包括API和全部源码) 发布
    jQuery liger ui ligerGrid 打造通用的分页排序查询表格(提供下载)
    jQuery LigerUI V1.1.5 (包括API和全部源码) 发布
    jQuery LigerUI 使用教程表格篇(1)
    jQuery LigerUI V1.0(包括API和全部源码) 发布
    jQuery LigerUI V1.1.0 (包括API和全部源码) 发布
    nginx keepalived
  • 原文地址:https://www.cnblogs.com/whileskies/p/13321497.html
Copyright © 2011-2022 走看看