序言(废话) : 在看书的过程中发现一开始不是很能理解 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. 也就是说实际上数据中有两个页面管理的数据结构(页目录表和页表合起来相当于一个管理页面的数据结构).
- 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, 12
push LenBar ; 被段 (但是以 ds 为段基址) 的长度
push OffsetBar ; 被段 (但是以 ds 为段基址) 的段偏移量
push ProcBar ; 目的代码段的物理空间地址 00501000h
- call MemCpy
- add esp, 12
- push LenPagingDemoAll
- push OffsetPagingDemoProc
- push ProcPagingDemo ; [es:ProcPagingDemo] = ProcPagingDemo = 00301000h
- call MemCpy
- add esp, 12
- mov ax, SelectorData
mov ds, ax ; 数据段选择子
- mov es, ax
call SetupPaging ; 启动分页
; 当前线性地址依然等于物理地址
call SelectorFlatC:ProcPagingDemo ; 访问的线性地址为 00301000h, 物理地址也是 00301000h
call PSwitch ; 切换页目录, 改变地址映射关系
call SelectorFlatC:ProcPagingDemo ; 访问的线性地址为 00301000h
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" 然后两次返回.
第二次分页 : 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]
.1: ; es:edi 初始等于 PageDirBase1 (当前页目录表项), eax 初始基地址等于 PageTblBase1
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .1
; 再初始化所有页表
mov eax, [PageTableNumber] ; 页表个数
mov ebx, 1024 ; 每个页表 1024 个 PTE
mul ebx
mov ecx, eax ; PTE 个数 = 页表个数 * 1024
mov edi, PageTblBase1 ; 此段首地址为 PageTblBase1
- xor eax, eax
- mov eax, PG_P | PG_USU | PG_RWW
.2: ; es:edi 初始等于 PageTblBase1 (当前页表项), eax 初始基地址等于 0(线性地址等于物理地址)
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .2
; 在此假设内存是大于 8M 的
; 下列代码将 LinearAddrDemo 所处的页表的相对第一个页表的偏移地址放入 ecx 中
- mov eax, LinearAddrDemo
- shr eax, 22
mov ebx, 4096 ; (LinearAddrDemo / 4M)表示第几个页表
mul ebx ; 第几个页表 * 4k (1024(一个页表项的数量) * 4(一个页表项的字节))
mov ecx, eax ; 也就是对应页表的偏移地址
; 下列代码将 LinearAddrDemo 所处的页表项相对第一个页表项的偏移地址放入 eax 中
mov eax, LinearAddrDemo
shr eax, 12 ; LinearAddrDemo / 4k, 表示第几个页表项
and eax, 03FFh ; 1111111111b (10 bits) ; 取低 10 位, 也就是余下的零散页表项(一个页表有 2^10 个页表项)
mov ebx, 4
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 .3
- .3:
- nop
- ret
在这里我加了几个比较重要的注释分别在第 9, 22, 28,35 处.
这段代码做了什么?
首先是设置页面管理的数据结构 (页表和页目录表), 但是需要注意的是, 这里设置页表和页目录表除了不是之前的页面管理结构之外, 其实内容是差不多的, 也就是说当前(第 25 行) 这里的状态也是 线性地址 = 物理地址 !!!
但是在第 27 行做了一个操作, 就是将 LinearAddrDemo 对应的 页表项的地址 换成了 ProcBar(00501000h) 的地址.(具体如何实现的请看 27-45 行我写的注释).
在做完这些之后就返回第二次执行 call SelectorFlatC:ProcPagingDemo 了, 在这个时候 cs = SelectorFlatC (段基址等于 0), eip = ProcPagingDemo = 00301000h, 也就是说访问了
线性地址 = 00301000h 处, 但是这里已经被修改, 除了这个页面之外, 其他页面都是 线性地址 = 物理地址, 但是这里 线性地址 = 00301000h , 映射的物理地址是 ProcBar(00501000h)
于是便调用了 ProcBar 段的代码, 而这段的代码是第二次调用 MemCpy 时候复制过去的. 被复制的具体代码是:
- 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 的?
通过将线性地址 = ProcPaging(00301000h)对应的页表项的地址值给修改成了 PaocBar(00501000h)的物理地址, 于是从 00301000h 的线性地址 映射到 00501000h 的物理地址上去了,
但是其实其他地方 (除了这个页之外) 的线性地址 = 物理地址依然成立. 也是上面这段代码很小, 一定是小于 4k(一页的大小), 于是只需要修改一个页表项就可以了!
来源: http://www.bubuko.com/infodetail-3303002.html