zoukankan      html  css  js  c++  java
  • x86汇编分页模式实验 --《ORANGE'S一个操作系统的实现》中 pmtest8.asm解析

      序言(废话) : 在看书的过程中发现一开始不是很能理解pmtest8的目的,以及书上说得很抽象..于是在自己阅读过源代码后,将一些自己的心得写在这里。

      正文 : 

      讲解顺序依然按照书上贴代码的顺序来。但是是几乎逐句解释的。可能会稍微有点啰嗦。废话就不多说了直接贴代码。

    LABEL_DESC_FLAT_C:  Descriptor 0,        0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
    LABEL_DESC_FLAT_RW: Descriptor 0,        0fffffh, DA_DRW|DA_LIMIT_4K     ; 0~4G
    SelectorFlatC       equ    LABEL_DESC_FLAT_C - LABEL_GDT                
    SelectorFlatRW        equ    LABEL_DESC_FLAT_RW - LABEL_GDT

      显然,两个分别是 FLAT_C 和  FLAT_RW 的描述符和选择子。

      问题 : 为什么要有这两个东西?

      解释 : FLAT_C是用来执行的非一致性32位代码段,粒度为4k,也就是 limit(段限长) = (0xfffff + 1)  * 4k = 4G,FLAT_RW 是用来修改数据的,因为需要利用这个描述符的权限(可写)来将代码写入到目的地(这个目的地允许在 0 - 4G区间内)。之所以要分两个选择符,是防止在执行的时候修改代码(所以FLAT_C不能给写的权限),但是又必须在执行之前进行复制,所以一定要有一个入口能提供写入的方式,于是设置两个描述符来进行。这样既安全又有章法。

    SetupPaging:
        ; 根据内存大小计算应初始化多少PDE以及多少页表
        xor    edx, edx
        mov    eax, [dwMemSize]
        mov    ebx, 400000h    ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小
        div    ebx
        mov    ecx, eax    ; 此时 ecx 为页表的个数,也即 PDE 应该的个数
        test    edx, edx
        jz    .no_remainder
        inc    ecx        ; 如果余数不为 0 就需增加一个页表
    .no_remainder:
        mov    [PageTableNumber], ecx    ; 暂存页表个数
    
        ; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
    
        ; 首先初始化页目录
        mov    ax, SelectorFlatRW
        mov    es, ax
        mov    edi, PageDirBase0    ; 此段首地址为 PageDirBase0
        xor    eax, eax
        mov    eax, PageTblBase0 | PG_P  | PG_USU | PG_RWW
    .1:    ; es:edi 初始等于 PageDirBase0 (当前页目录表项), eax 初始基地址等于 PageTblBase0
        stosd
        add    eax, 4096        ; 为了简化, 所有页表在内存中是连续的.
        loop    .1
    
        ; 再初始化所有页表
        mov    eax, [PageTableNumber]    ; 页表个数
        mov    ebx, 1024        ; 每个页表 1024 个 PTE
        mul    ebx
        mov    ecx, eax        ; PTE个数 = 页表个数 * 1024
        mov    edi, PageTblBase0    ; 此段首地址为 PageTblBase0
        xor    eax, eax
        mov    eax, PG_P  | PG_USU | PG_RWW
    .2:    ; es:edi 初始等于 PageTblBase0 (当前页表项), eax = 0 (线性地址 = 物理地址)
        stosd
        add    eax, 4096        ; 每一页指向 4K 的空间
        loop    .2
    
        mov    eax, PageDirBase0
        mov    cr3, eax
        mov    eax, cr0
        or    eax, 80000000h
        mov    cr0, eax
        jmp    short .3
    .3:
        nop
    
        ret

      这段代码我加注了两句注释 分别在 .1 和 .2 这两个标签那行,其实这里和之前的setPaging并没有很大的区别,需要注意的就是 这里的 页目录表 的地址是  PageDirBase0, 页表的地址是PageTblBase0,强调这点的原因在于之后的  PSwitch 这个函数中则是 PageDirBase1 和 PageTblBase1。也就是说实际上数据中有两个页面管理的数据结构(页目录表和页表合起来相当于一个管理页面的数据结构)。

     1 PagingDemo:
     2     mov    ax, cs
     3     mov    ds, ax
     4     mov    ax, SelectorFlatRW        ; 设置es为基地址为0的可读写的段(便于复制代码)
     5     mov    es, ax
     6     
     7     push    LenFoo
     8     push    OffsetFoo
     9     push    ProcFoo            ; 00401000h
    10     call    MemCpy        
    11     add    esp, 12
    12 
    13     push    LenBar            ; 被复制代码段(但是以ds为段基址)的长度 
    14     push    OffsetBar        ; 被复制代码段(但是以ds为段基址)的段偏移量
    15     push    ProcBar            ; 目的代码段的物理空间地址 00501000h
    16     call    MemCpy
    17     add    esp, 12
    18 
    19     push    LenPagingDemoAll
    20     push    OffsetPagingDemoProc    
    21     push    ProcPagingDemo            ; [es:ProcPagingDemo] = ProcPagingDemo = 00301000h
    22     call    MemCpy
    23     add    esp, 12
    24 
    25     mov    ax, SelectorData
    26     mov    ds, ax            ; 数据段选择子
    27     mov    es, ax
    28 
    29     call    SetupPaging        ; 启动分页
    30     ; 当前线性地址依然等于物理地址
    31     call    SelectorFlatC:ProcPagingDemo   
    32     call    PSwitch            ; 切换页目录,改变地址映射关系
    33     call    SelectorFlatC:ProcPagingDemo  
    34 
    35     ret

      在这里首先要说明的是 MemCpy函数,这个函数有三个参数分别表示 : 

       1)被复制段(但是以ds为段基址)的 长度 
       2)被复制段(但是以ds为段基址)的 段偏移量
       3)目的地的物理空间地址(之所以说是物理空间是因为当前线性地址等于物理地址,以es为段基址,但是es的段基址为0)
    功能则是 将被复制段 的数据复制 参数1)的长度字节 去目的地去(简单说就是利用三个参数复制数据)

    我们可以知道的是在上面代码中三次调用 MemCpy 都没有进入分页模式,也就是说当下线性地址等于物理地址。那么根据我上面的注释就可以知道三个代码分别复制到哪里去了。
    之后就是恢复数据段(之前将ds = cs,是为了复制代码),然后启动分页(上面已经讲了),然后启动分页后当前线性地址依然等于物理地址。
    这个时候第一次调用 call SelectorFlatC:ProcPagingDemo,也就是访问的线性地址为 00301000h,物理地址也是 00301000h的代码(之前移动过去的)。
     下面这段代码就是被移动到00301000h的代码,这段代码只做了一件事那就是调用 [cs:LinearAddrDemo]的代码,但请注意,由于 call SelectorFlatC:ProcPagingDemo
    所以此时的 cs = SelectorFlatC,也就是说段基址等于0,于是实际上这段代码的功能就是访问 物理地址为00401000h处的代码。
    PagingDemoProc:
    OffsetPagingDemoProc    equ    PagingDemoProc - $$
        mov    eax, LinearAddrDemo
        call    eax        ; 未开始PSwitch前, eax = ProcFoo = 00401000h (cs 的段基址 = 0)
        retf
    LenPagingDemoAll    equ    $ - PagingDemoProc
    
    

      而物理地址00401000h处就是ProcFoo的代码(第一次调用MemCpy拷贝的代码)。被拷贝的代码如下

    foo:
    OffsetFoo        equ    foo - $$
        mov    ah, 0Ch            ; 0000: 黑底    1100: 红字
        mov    al, 'F'
        mov    [gs:((80 * 17 + 0) * 2)], ax    ; 屏幕第 17 行, 第 0 列。
        mov    al, 'o'
        mov    [gs:((80 * 17 + 1) * 2)], ax    ; 屏幕第 17 行, 第 1 列。
        mov    [gs:((80 * 17 + 2) * 2)], ax    ; 屏幕第 17 行, 第 2 列。
        ret
    LenFoo            equ    $ - foo

      功能很明显就是现实一个字符串 Foo而已。

    总结第一次分页后的动作:

      就是拷贝三份代码分别到ProcFoo, ProcBar, ProcPagingDemo 处(这四个都是物理内存哦,并且后面因为段基址是0(FLAT_C 段基址)于是很容易地就访问到了物理地址)。然后开启分页模式(其实几乎没什么影响 因为仍然和分段一样 线性地址 = 物理地址)。然后调用 被拷贝的函数 ProcPagingDemo ,ProcPagingDemo 函数调用 ProcFoo函数,显示字符 "Foo"然后两次返回(ret)。

    修改页表后 : call PSwitch

    被调用代码如下 :

     1 PSwitch:
     2     ; 初始化页目录
     3     mov    ax, SelectorFlatRW
     4     mov    es, ax
     5     mov    edi, PageDirBase1    ; 此段首地址为 PageDirBase1
     6     xor    eax, eax
     7     mov    eax, PageTblBase1 | PG_P  | PG_USU | PG_RWW
     8     mov    ecx, [PageTableNumber]
     9 .1:    ; es:edi 初始等于 PageDirBase1 (当前页目录表项), eax 初始基地址等于 PageTblBase1
    10     stosd
    11     add    eax, 4096        ; 为了简化, 所有页表在内存中是连续的.
    12     loop    .1
    13 
    14     ; 再初始化所有页表
    15     mov    eax, [PageTableNumber]    ; 页表个数
    16     mov    ebx, 1024        ; 每个页表 1024 个 PTE
    17     mul    ebx
    18     mov    ecx, eax        ; PTE个数 = 页表个数 * 1024
    19     mov    edi, PageTblBase1    ; 此段首地址为 PageTblBase1
    20     xor    eax, eax
    21     mov    eax, PG_P  | PG_USU | PG_RWW
    22 .2: ; es:edi 初始等于 PageTblBase1 (当前页表项), eax 初始基地址等于 0(线性地址等于物理地址)
    23     stosd
    24     add    eax, 4096        ; 每一页指向 4K 的空间
    25     loop    .2
    26 
    27     ; 在此假设内存是大于 8M 的
    28     ; 下列代码将LinearAddrDemo所处的页表的相对第一个页表的偏移地址放入ecx中
    29     mov    eax, LinearAddrDemo
    30     shr    eax, 22
    31     mov    ebx, 4096        ; (LinearAddrDemo / 4M)表示第几个页表
    32     mul    ebx                ; 第几个页表 * 4k (1024(一个页表项的数量) * 4(一个页表项的字节))
    33     mov    ecx, eax        ; 也就是对应页表的偏移地址
    34     
    35     ; 下列代码将LinearAddrDemo所处的页表项相对第一个页表项的偏移地址放入eax中
    36     mov    eax, LinearAddrDemo
    37     shr    eax, 12            ; LinearAddrDemo / 4k,表示第几个页表项
    38     and    eax, 03FFh    ; 1111111111b (10 bits)    ; 取低10位,也就是余下的零散页表项(一个页表有2^10个页表项)
    39     mov    ebx, 4                                
    40     mul    ebx                                    ; * 4 表示的是具体偏移字节数
    41     add    eax, ecx                            ; eax = (((LinearAddrDemo / 2^12) & 03FFh) * 4) + (4k * (LinearAddrDemo / 2^22))
    42     
    43     
    44     add    eax, PageTblBase1                    ; 第一个页表的第一个页表项
    45     mov    dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
    46 
    47     mov    eax, PageDirBase1
    48     mov    cr3, eax
    49     jmp    short .3
    50 .3:
    51     nop
    52 
    53     ret

      在这里我加了几个比较重要的注释分别在第 9, 22, 28,35处。

      这段代码做了什么?

      首先是设置页面管理的数据结构(页表和页目录表),但是需要注意的是,这里设置页表和页目录表除了不是之前的页面管理结构之外,其实内容是差不多的,也就是说当前(第25行)这里的状态也是 线性地址 = 物理地址 !!!

     但是在第27行做了一个操作,就是将LinearAddrDemo对应的 页表项的地址(00401000h) 换成了 ProcBar(00501000h) 的地址。(具体如何实现的请看27-45行我写的注释)。
      在做完这些之后就返回第二次执行 call SelectorFlatC:ProcPagingDemo 了,在这个时候 cs = SelectorFlatC (段基址等于0), eip = ProcPagingDemo = 00301000h,也就是说访问了
    线性地址 = 物理地址 = 00301000h 处,然后开始调用
     mov    eax, LinearAddrDemo
     call    eax

    但是第二次调用时这里已经被修改,当call eax 时候,跳转的线性地址应该是 0:00401000h,但是访问的物理地址却是 0:00501000h
    于是便调用了 ProcBar 段的代码,而这段的代码是第二次调用MemCpy时候复制到物理地址 0:0x501000h 的。被复制的具体代码是:
    bar:
    OffsetBar        equ    bar - $$
        mov    ah, 0Ch            ; 0000: 黑底    1100: 红字
        mov    al, 'B'
        mov    [gs:((80 * 18 + 0) * 2)], ax    ; 屏幕第 18 行, 第 0 列。
        mov    al, 'a'
        mov    [gs:((80 * 18 + 1) * 2)], ax    ; 屏幕第 18 行, 第 1 列。
        mov    al, 'r'
        mov    [gs:((80 * 18 + 2) * 2)], ax    ; 屏幕第 18 行, 第 2 列。
        ret
    LenBar            equ    $ - bar
    也就是显示一个字符串 "Bar", 然后返回到PagingDemo的最后一句 ret,再次返回。于是这段代码也就结束了。
    第二次代码是如何实现调用 ProcBar的?
      通过将线性地址 = PaocFoo (00401000h)对应的页表项的地址值给修改成了 PaocBar(00501000h)的物理地址,于是从 00401000h 的线性地址 映射到 00501000h的物理地址上去了,
    但是其实其他地方(除了这个页之外)的线性地址 = 物理地址依然成立。也是上面这段代码很小,一定是小于 4k(一页的大小),于是只需要修改一个页表项就可以了!
     
  • 相关阅读:
    函数即变量
    装饰器模型
    团队配合指令
    三元指令
    虚实之门
    for的逻辑
    我写的第4个程序(日志最近行读取函数)
    还在用WebBrowser吗?你out了!
    关于打印机共享的注意事项——又被叫去修电脑了
    MVVM转换器Int2StringConverter基础类
  • 原文地址:https://www.cnblogs.com/vizdl/p/11925205.html
Copyright © 2011-2022 走看看