• 12 实模式到保护模式 下


    参考

    https://www.cnblogs.com/wanmeishenghuo/tag/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/

    https://blog.51cto.com/13475106/category6.html

     

    这一节,我们深入研究一下保护模式:定义显存段

    为了显示数据,必须存在两大硬件:显卡+显示器

    显卡:

      1、为显示器提供需要显示的数据

      2、控制显示器的模式和状态

    显示器:

      1、将目标数据以可见的方式呈现在屏幕上

    显存的概念和意义:

    1、显卡拥有自己内部的数据存储器,简称显存

    2、显存在本质上和普通内存无差别,用于存储目标数据

    3、操作显存中的数据将导致显示器上的内容改变

    显存在本质上和内存没有差别,只不过它存在与显卡的内部

    显卡的工作模式有文本模式和图形模式:

      在不同的模式下,显卡对显存内容的解释是不同的

      可以使用专属指令或者int 0x10中断改变显卡的工作模式

      在文本模式下:

        显存的地址范围映射为:[0xB8000,0xBFFFF],直接往这个地址写数据,显示器上就会显示数据

        一屏幕可以显示25行,每行80个字符,每个字符占两个字节,一个字节为实际的字符(低字节),一个字节为字符的属性(高字节)

     下面我们来完成在屏幕的指定位置上打印指定字符串的功能,在保护模式下打印指定内存中的字符串,步骤如下:

      定义全局堆栈段(.gs),用于保护模式下的函数调用

      定义全局数据段(.dat),用于定义只读数据(D.T.OS!)

      利用对显存段的操作定义字符串打印函数(PrintString)

    汇编知识:

      32位保护模式下的乘法操作(mul):

        被乘数放到AX寄存器

        乘数放到通用寄存器或内存单元(16位)

        相乘的结果放到EAX寄存器中

      再论$$和$:

        $表示当前行相对于代码起始位置处的偏移量

        $$表示当前代码节(section)的起始位置

    下面直接给出打印字符串的程序:

    %include "inc.asm"
    
    org 0x9000
    
    jmp CODE16_SEGMENT
    
    [section .gdt]
    ; GDT definition
    ;                                 段基址,       段界限,       段属性
    GDT_ENTRY       :     Descriptor    0,            0,           0
    CODE32_DESC     :     Descriptor    0,    Code32SegLen - 1,    DA_C + DA_32
    VIDEO_DESC      :     Descriptor 0xB8000,     0x07FFF,         DA_DRWA + DA_32
    DATA32_DESC     :     Descriptor    0,    Data32SegLen - 1,    DA_DR + DA_32
    STACK_DESC      :     Descriptor    0,     TopOfStackInit,     DA_DRW + DA_32
    ; GDT end
    
    GdtLen    equ   $ - GDT_ENTRY
    
    GdtPtr:
              dw   GdtLen - 1
              dd   0
              
              
    ; GDT Selector
    
    Code32Selector    equ (0x0001 << 3) + SA_TIG + SA_RPL0
    VideoSelector     equ (0x0002 << 3) + SA_TIG + SA_RPL0
    Data32Selector    equ (0x0003 << 3) + SA_TIG + SA_RPL0
    StackSelector     equ (0x0004 << 3) + SA_TIG + SA_RPL0
    
    ; end of [section .gdt]
    
    TopOfStackInit    equ 0x7c00
    
    [section .dat]
    [bits 32]
    DATA32_SEGMENT:
        DTOS               db  "D.T.OS!", 0
        DTOS_OFFSET        equ DTOS - $$
        HELLO_WORLD        db  "Hello World!", 0
        HELLO_WORLD_OFFSET equ HELLO_WORLD - $$
    
    Data32SegLen equ $ - DATA32_SEGMENT
    
    [section .s16]
    [bits 16]
    CODE16_SEGMENT:
        mov ax, cs
        mov ds, ax
        mov es, ax
        mov ss, ax
        mov sp, TopOfStackInit
        
        ; initialize GDT for 32 bits code segment
        mov esi, CODE32_SEGMENT
        mov edi, CODE32_DESC
        
        call InitDescItem
        
        mov esi, DATA32_SEGMENT
        mov edi, DATA32_DESC
        
        call InitDescItem
        
        ; initialize GDT pointer struct
        mov eax, 0
        mov ax, ds
        shl eax, 4
        add eax, GDT_ENTRY
        mov dword [GdtPtr + 2], eax
    
        ; 1. load GDT
        lgdt [GdtPtr]
        
        ; 2. close interrupt
        cli 
        
        ; 3. open A20
        in al, 0x92
        or al, 00000010b
        out 0x92, al
        
        ; 4. enter protect mode
        mov eax, cr0
        or eax, 0x01
        mov cr0, eax
        
        ; 5. jump to 32 bits code
        jmp dword Code32Selector : 0
    
    
    ; esi    --> code segment label
    ; edi    --> descriptor label
    InitDescItem:
        push eax
    
        mov eax, 0
        mov ax, cs
        shl eax, 4
        add eax, esi
        mov word [edi + 2], ax
        shr eax, 16
        mov byte [edi + 4], al
        mov byte [edi + 7], ah
        
        pop eax
        
        ret
        
        
    [section .s32]
    [bits 32]
    CODE32_SEGMENT:
        mov ax, VideoSelector
        mov gs, ax
        
        mov ax, StackSelector
        mov ss, ax
        
        mov ax, Data32Selector
        mov ds, ax
        
        mov ebp, DTOS_OFFSET
        mov bx, 0x0C
        mov dh, 12
        mov dl, 33
        
        call PrintString
        
        mov ebp, HELLO_WORLD_OFFSET
        mov bx, 0x0C
        mov dh, 13
        mov dl, 31
        
        call PrintString
        
        jmp $
    
    ; ds:ebp    --> string address
    ; bx        --> attribute
    ; dx        --> dh : row, dl : col
    PrintString:
        push ebp
        push eax
        push edi
        push cx
        push dx
        
    print:
        mov cl, [ds:ebp]
        cmp cl, 0
        je end
        mov eax, 80
        mul dh
        add al, dl
        shl eax, 1
        mov edi, eax
        mov ah, bl
        mov al, cl
        mov [gs:edi], ax
        inc ebp
        inc dl
        jmp print
    
    end:
        pop dx
        pop cx
        pop edi
        pop eax
        pop ebp
        
        ret
        
    Code32SegLen    equ    $ - CODE32_SEGMENT
    

     

    对上述程序做一个解析:

      第12行定义了显存段,这个段可以直接根据起始地址和段界限给出

      第13行定义了32位的数据段,其中的起始地址需要在运行时计算,因为具体的起始物理地址和段寄存器有关,而段界限可以在编译时期计算出来

      第14行定义了保护模式下的堆栈段

    定义了段描述符后必然要定义相应的段选择子,第27、28、29分别定义了对应上述三个段的段选择子。

    保护模式下的堆栈栈顶依然定义为0x7c00。

    32位数据段中(35-43行)定义了一些需要打印的字符串。

    16位实模式下的代码和上一节几乎一样,这一节我们需要在运行期初始化两个段描述符中的基地址,因此,我们将初始化的过程封装成了函数InitDescItem,这个函数需要两个参数,esi代表代码段或者数据段的标签,edi代表段描述符的标签。有了这个函数之后,55-63行就可以方便的调用这个函数来初始化段描述符中的基地址了。

      下面进入32位代码段,从111行开始,114-121行,分别将相应的段选择子存入相应的段寄存器中,显存段存入了gs,堆栈段存入了ss,数据段存入了ds。当需要用到堆栈时,CPU自动根据ss和sp的值计算真正的物理地址,sp在16位代码中初始化为了栈顶,虽然在16位代码中有函数调用,但是向32位代码跳转时这些函数已经全部返回了,因此,在32位代码的起始处,sp还是指向栈顶的。当取32数据段中的字符时,我们显式的使用ds作为段寄存器。当向显存写数据时,我们显式的使用gs作为段寄存器,这样可以保证地址计算不会出错。有些指令会有默认的段寄存器,但是为了保险我们显式的指定。这在打印函数中会看到。

      32位代码段设置完几个段寄存器后就开始调用打印函数了,先是将参数写入相应的寄存器,然后调用函数。我们分析一下130-135行的调用过程:

      第130行将字符串“Hello World!”的起始地址相对于32位数据段的偏移量存入ebp,然后在其他几个寄存器存入打印属性和打印的行和列,然后调用打印函数。

      下面进入139-172行打印字符串的函数,它是在32位代码段中的,这个函数接受三个参数,ds:ebp存放字符串地址,bx存放属性,dx存放行和列,也就是要打印在第几行第几列。

      我们看一下具体的打印过程:

      142-147行将一些寄存器先保存起来,第150行中将目标地址中的字符取出来,目标地址是根据 段+偏移 的方式算出来的,mov cl, [ds:ebp]指令中,我们显式的指明段寄存器为ds,ds中存放的是32位数据段的选择子,这也是我们在前面初始化好了的,这样可以保证计算出的字符的地址是正确的。151行判断要打印的字符是否为0,为0就不打印了,跳到end,不为零的话就继续根据行和列的值计算出地址(这个地址是相对于显存起始地址的偏移量),把这个偏移量存入edi寄存器, 第160行使用指令mov [gs:edi], ax将字符写入显存中,这里的显存物理地址是根据gs和edi计算出来的,我们显式的指明gs作为段选择子,其中存放的是显存段的选择子,这也是我们在前面初始化好了的,这样可以保证写入到正确的位置。 

    打印字符串的效果如下:

      我们来看一下92-108行,这一段代码作用是计算某个段的物理基地址,并写入到段描述符中。esi存放段标签,edi存放的是段描述符标签。

    假设现在esi存放了DATA32_SEGMENT的地址,edi存放的是DATA32_DESC。计算段的物理基地址时使用了eax(里面是cs的值),这样计算出了物理地址,向段描述符中写时使用的是mov byte [edi + 4], al 指令,edi中的值是相对于0x9000计算出来的值,而真正的物理地址还需要根据ds段寄存器的值计算出来(此时处于16位实模式,段寄存器存放的就是段基地址),此时的ds中的值必须和cs中的值一样才行,这在48-51行也保证了,如果ds的值不等于cs的话,程序会发生错误(已经实验验证)。

      在boot.asm程序中,我们也是将cs,ds,ss等段寄存器弄成了一样的值,在加载loader.asm时,我们只给出了偏移地址,不管段基址用哪一个计算,结果都是一样的,我们将loader加载到了0x9000处,如果段寄存器中的值是0的话,那就真正加载到了物理地址的0x9000处,boot.asm有一句跳转到指定地址的指令jnb 0x9000,如果段寄存器为0,它就可以跳到物理地址0x9000处,和实际的加载地址正好对应上。如果段寄存器不为0,它就要根据cs寄存器计算真正的物理地址,而如果cs不为0,则ds和ss等也不为0(它们三个是一样的值,代码中有赋值操作),而计算出来的物理加载地址也就不是0x9000,但是不管是多少,只要保证jnb跳转时的偏移地址和加载loader.asm时的偏移地址是一样的就行(程序中都是0x9000),这样就不会出错,因为段寄存器中的值都是一样的,计算出来的物理地址肯定也是一样的。

      到了loader.asm中,又对几个段寄存器进行了一次操作,还是要保证cs,ds,ss的值是一样的,这样就跟上面标红的一段对应起来了,使用cs计算一个段的真实物理起始地址可以得到正确的值(这个值本应该按ds计算,因为加载loader.asm是按ds计算的,但是cs和ds相等,所以结果一样),使用ds计算段描述符的起始地址也可以得到描述符正确的值,mov byte [edi+4], al中默认使用ds作为段寄存器。标签的值是相对于代码的起始地址0x9000计算出来的地址,如果段寄存器都为0,标签的值等于真实的物理地址。如果段寄存器不为0,只要保证它们在实模式下都相等也可以,这样它们也算有一个统一的“0”地址。

           将编译后生成的data.img放到windows下的虚拟机路径,选择从这个光驱文件移动,也可看到如下结果:

    总结:

     

  • 相关阅读:
    科学计算和可视化,做数据分析与雷达图。
    Leetcode 429 N叉树的层序遍历
    Leetcode 867转置矩阵
    Leetcode 832 翻转图像
    Leetcode 1052 爱生气的书店老板
    Leetcode 337打家劫舍 III
    Leetcode 766 托普利茨矩阵
    Leetcode 1438绝对差不超过限制的最长连续子数组
    Leetcode 697 数组的度
    Leetcode 567 字符串的排列
  • 原文地址:https://www.cnblogs.com/lh03061238/p/14072587.html
Copyright © 2020-2023  润新知