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

  正文 :

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

LABEL_DESC_FLAT_C:  Descriptor ,        0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
LABEL_DESC_FLAT_RW: Descriptor , 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
.: ; es:edi 初始等于 PageDirBase0 (当前页目录表项), eax 初始基地址等于 PageTblBase0
stosd
add eax, ; 为了简化, 所有页表在内存中是连续的.
loop . ; 再初始化所有页表
mov eax, [PageTableNumber] ; 页表个数
mov ebx, ; 每个页表 1024 个 PTE
mul ebx
mov ecx, eax ; PTE个数 = 页表个数 * 1024
mov edi, PageTblBase0 ; 此段首地址为 PageTblBase0
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.: ; es:edi 初始等于 PageTblBase0 (当前页表项), eax = 0 (线性地址 = 物理地址)
stosd
add eax, ; 每一页指向 4K 的空间
loop . mov eax, PageDirBase0
mov cr3, eax
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .
.:
nop ret

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

 PagingDemo:
mov ax, cs
mov ds, ax
mov ax, SelectorFlatRW ; 设置es为基地址为0的可读写的段(便于复制代码)
mov es, ax push LenFoo
push OffsetFoo
push ProcFoo ; 00401000h
call MemCpy
add esp, push LenBar ; 被复制代码段(但是以ds为段基址)的长度
push OffsetBar ; 被复制代码段(但是以ds为段基址)的段偏移量
push ProcBar ; 目的代码段的物理空间地址 00501000h
call MemCpy
add esp, push LenPagingDemoAll
push OffsetPagingDemoProc
push ProcPagingDemo ; [es:ProcPagingDemo] = ProcPagingDemo = 00301000h
call MemCpy
add esp, mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov es, ax call SetupPaging ; 启动分页
; 当前线性地址依然等于物理地址
call SelectorFlatC:ProcPagingDemo
call PSwitch ; 切换页目录,改变地址映射关系
call SelectorFlatC:ProcPagingDemo 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:(( * + ) * )], ax ; 屏幕第 17 行, 第 0 列。
mov al, 'o'
mov [gs:(( * + ) * )], ax ; 屏幕第 17 行, 第 1 列。
mov [gs:(( * + ) * )], ax ; 屏幕第 17 行, 第 2 列。
ret
LenFoo equ $ - foo

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

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

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

修改页表后 : call PSwitch

被调用代码如下 :

 PSwitch:
; 初始化页目录
mov ax, SelectorFlatRW
mov es, ax
mov edi, PageDirBase1 ; 此段首地址为 PageDirBase1
xor eax, eax
mov eax, PageTblBase1 | PG_P | PG_USU | PG_RWW
mov ecx, [PageTableNumber]
.: ; es:edi 初始等于 PageDirBase1 (当前页目录表项), eax 初始基地址等于 PageTblBase1
stosd
add eax, ; 为了简化, 所有页表在内存中是连续的.
loop . ; 再初始化所有页表
mov eax, [PageTableNumber] ; 页表个数
mov ebx, ; 每个页表 1024 个 PTE
mul ebx
mov ecx, eax ; PTE个数 = 页表个数 * 1024
mov edi, PageTblBase1 ; 此段首地址为 PageTblBase1
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.: ; es:edi 初始等于 PageTblBase1 (当前页表项), eax 初始基地址等于 0(线性地址等于物理地址)
stosd
add eax, ; 每一页指向 4K 的空间
loop . ; 在此假设内存是大于 8M 的
; 下列代码将LinearAddrDemo所处的页表的相对第一个页表的偏移地址放入ecx中
mov eax, LinearAddrDemo
shr eax,
mov ebx, ; (LinearAddrDemo / 4M)表示第几个页表
mul ebx ; 第几个页表 * 4k (1024(一个页表项的数量) * 4(一个页表项的字节))
mov ecx, eax ; 也就是对应页表的偏移地址 ; 下列代码将LinearAddrDemo所处的页表项相对第一个页表项的偏移地址放入eax中
mov eax, LinearAddrDemo
shr eax, ; LinearAddrDemo / 4k,表示第几个页表项
and eax, 03FFh ; 1111111111b (10 bits) ; 取低10位,也就是余下的零散页表项(一个页表有2^10个页表项)
mov ebx,
mul ebx ; * 4 表示的是具体偏移字节数
add eax, ecx ; eax = (((LinearAddrDemo / 2^12) & 03FFh) * 4) + (4k * (LinearAddrDemo / 2^22)) add eax, PageTblBase1 ; 第一个页表的第一个页表项
mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW mov eax, PageDirBase1
mov cr3, eax
jmp short .
.:
nop 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:(( * + ) * )], ax ; 屏幕第 18 行, 第 0 列。
mov al, 'a'
mov [gs:(( * + ) * )], ax ; 屏幕第 18 行, 第 1 列。
mov al, 'r'
mov [gs:(( * + ) * )], ax ; 屏幕第 18 行, 第 2 列。
ret
LenBar equ $ - bar
也就是显示一个字符串 "Bar", 然后返回到PagingDemo的最后一句 ret,再次返回。于是这段代码也就结束了。
第二次代码是如何实现调用 ProcBar的?
  通过将线性地址 = PaocFoo (00401000h)对应的页表项的地址值给修改成了 PaocBar(00501000h)的物理地址,于是从 00401000h 的线性地址 映射到 00501000h的物理地址上去了,
但是其实其他地方(除了这个页之外)的线性地址 = 物理地址依然成立。也是上面这段代码很小,一定是小于 4k(一页的大小),于是只需要修改一个页表项就可以了!
 

x86汇编分页模式实验 --《ORANGE'S一个操作系统的实现》中 pmtest8.asm解析的更多相关文章

  1. 《Orange'S:一个操作系统的实现》笔记(一)

    感觉自己对于操作系统始终没有一个清楚的概念,尤其最近困扰于实模式.保护模式以及寻址方式等一些概念.转而一想,所有的程序,最终都是操作的计算机资源,需要和操作系统打交道,所以操作系统有必要深入了解一下. ...

  2. 配置《Orange's一个操作系统的实现》环境心得

    <Orange>这本书开篇第一章就做了一个实例,编写了一段引导扇区的代码,但是引导介质仍然采用了已被淘汰多年的软盘.在经历了两天的痛苦查找后终于找到了最方便的解决办法,在此做一下记录,希望 ...

  3. 《Orange’s 一个操作系统的实现》1.搭建操作系统开发环境

    书中给出了两种环境:windows和linux,平台选择根据自己喜好.本人这里选择ubuntu10.04+virtualbox作为开发平台. 1.下载.安装VirtualBox     http:// ...

  4. oslab oranges 一个操作系统的实现 实验三 认识保护模式(二):分页

    实验目的: 掌握内存分页机制 对应章节:3.3 实验内容: 1.认真阅读章节资料,掌握什么是分页机制 2. 调试代码,掌握分页机制基本方法与思路 – 代码3.22中,212行---237行,设置断点调 ...

  5. oslab oranges 一个操作系统的实现 实验五 让操作系统走进保护模式

    实验目的: • 如何从软盘读取并加载一个Loader程序到操作 系统,然后转交系统控制权 • 对应章节:第四章 实验内容: 1. 向软盘镜像文件写入一个你指定的文件,手 工读取在磁盘中的信息 2. 在 ...

  6. oslab oranges 一个操作系统的实现 实验二 认识保护模式

    https://github.com/yyu/osfs00 实验目的: 理解x86架构下的段式内存管理 掌握实模式和保护模式下段式寻址的组织方式. 关键数据结构.代码组织方式 掌握实模式与保护模式的切 ...

  7. oslab oranges 一个操作系统的实现 实验四 认识保护模式(三):中断异常

    实验目的: 理解中断与异常机制的实现机理 对应章节:第三章3.4节,3.5节 实验内容: 1. 理解中断与异常的机制 2. 调试8259A的编程基本例程 3. 调试时钟中断例程 4. 建立IDT,实现 ...

  8. 寄存器理解 及 X86汇编入门

    本文整理自多材料源,感谢原址分享,请查看末尾Url I, 汇编语言分类: 汇编语言和CPU息息相关,但是不能把汇编语言完全等同于CPU的机器指令.不同架构的CPU指令并不相同,如x86,powerpc ...

  9. 对X86汇编的理解与入门

    本文描述基本的32位X86汇编语言的一个子集,其中涉及汇编语言的最核心部分,包括寄存器结构,数据表示,基本的操作指令(包括数据传送指令.逻辑计算指令.算数运算指令),以及函数的调用规则.个人认为:在理 ...

随机推荐

  1. java日志空指针怎么定位问题

    示例报错: java.lang.NullPointerException: null at com.ipharmacare.sf.task.service.MatchAuditPlanService. ...

  2. Java中线程与堆栈的关系

    栈是线程私有的,每个线程都是自己的栈,每个线程中的每个方法在执行的同时会创建一个栈帧用于存局部变量表.操作数栈.动态链接.方法返回地址等信息.每一个方法从调用到执行完毕的过程,就对应着一个栈帧在虚拟机 ...

  3. 【原创】docker在Ubuntu下1小时快速学习

    前言 由于工作原因,很多情况下需要快速学习新的知识,针对docker如果从头到尾看相关书籍学习会非常慢,所以整理了下docker的常用操作,只要跟着本文学习操作,一小时就能掌握docker大部最常用分 ...

  4. 刷14道leetcode的总结

    引子 为什么我要刷leetcode?换工作?不是!那是?玩!巴菲特的双目标清单系统,基本方法是列两个清单,一个是职业生涯最重要的目标(不超过5个),另一个是比较重要的目标.对于比较重要的目标,要像躲避 ...

  5. 【Bug】解决 SpringBoot Artifact contains illegal characters 错误

    解决 SpringBoot  Artifact contains illegal characters错误 错误原因:Artifact包含非法字符(大写字母) 解决方法:将Artifact名称改成小写 ...

  6. mp-vue拖拽组件的实现

    作为一个效率还不错的小前端,自己的任务做完之后真的好闲啊,千盼万盼终于盼来了业务的新需求,他要我多加一个排序题,然后用户通过拖拽来排序,项目经理看我是个实习生,说有点复杂做不出来就算了,我这么闲的一个 ...

  7. 冷知识: 不会出现OutOfMemoryError的内存区域

    程序计数器(PC) 因为程序计数器只是记录当前线程正在执行的那条字节码指令的地址,即使出现死循环都不会内存溢出

  8. java中的无穷大和无穷小

    double型和float型都可以如下表示无穷大和无穷小 import static java.lang.Double.NEGATIVE_INFINITY;import static java.lan ...

  9. Java IO编程——File文件操作类

    在Java语言里面提供有对于文件操作系统操作的支持,而这个支持就在java.io.File类中进行了定义,也就是说在整个java.io包里面,File类是唯一 一个与文件本身操作(创建.删除.重命名等 ...

  10. OptimalSolution(1)--递归和动态规划(3)数组和字符串问题

    一.最长递增子序列(LIS) 给定数组arr,返回arr的最长递增子序列.例如,arr={2,1,5,3,6,4,8,9,7},返回的最长递增子序列为{1,3,4,5,8,9} 1.时间复杂度为O(N ...