ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

时间:2022-12-31 07:53:49

  第16章讲的是分页机制和动态页面分配的问题,说实话这个一开始接触是会把人绕晕的,但是这个的确太重要了,有了分页机制内存管理就变得很简单,而且能直接实现平坦模式。

★PART1:Intel X86基础分页机制

1. 页目录、页表和页

  首先先要明白分页是怎么来的,简单来讲,分页其实就是内存块的映射管理。在我们之前的章节中,我们都是使用的分段管理模式,处理器中负责分段的部件是段部件,段管理机制是Intel处理器最基本的处理机制,在任何时候都是无法关闭的。而当开启了分页管理之后,处理器会把4GB的内存分成长度相同的段,也就是说用长度固定的页来代替长度不一的段。页的分配由处理器固件来进行,可以实现非常高效的操作。页的最小单位是4KB,也就是说4GB的内存可以分成1048576个页(页的物理地址的低12位全是0)。

  在分页模式下,操作系统可以创建一个为所有任务共用的4GB虚拟内存空间。也可以为每一个任务创建一个独立的4GB虚拟内存空间,当一个程序加载的时候,操作系统既要在虚拟内存空间中分配空间,而且也要在物理内存中分配相应的也页面。另外,如果允许页共享,多个段或者多个程序可以用同一个页来存放各自的数据。4GB虚拟内存空间只是一个用来指示内存使用状况的一个机制,当操作系统加载一个程序并且创建为任务时,操作系统在虚拟内存空间中寻找空闲的段,并映射到空闲的页中取去。然后,到真正加载程序的时候,再把原本属于段的数据按页的尺寸拆分,分开写入相应的页中。页最大的好处就是方便操作系统进行内存管理,特别是虚拟内存管理,每个任务都可以有4GB虚拟内存,但是假如机器没有那么大的内存,操作系统也可以认为确实存在那么大的内存,当一个程序使用的内存超过了实际的物理内存,那么操作系统就会搜索那些暂时用不到的页,并且把他们转移到磁盘中,并且调入马上要使用的页。(当然这种操作非常地花费时间,这就是为什么小内存的电脑会有很严重的卡顿现象)。

  Intel处理器的最基础的分页管理机制就是二级管理机制。(当然现在更新的处理器支持更复杂的分页操作,但是教材也没有提及。)如果每个操使用直接分页管理(也就是把4GB的内存直接分成1048576个页),这需要1048576个表项,每个表项是4字节,所以映射表的总大小是4MB。事实上程序往往用不到4GB的内存,所以1级映射是一个很严重的浪费。但是如果采用层次化分页管理,那么就会产生巨大的内存节约。所谓Intel的分页管理,其实就是把页拆成3个部分(页目录,页表和页)。

  页目录(Page Directory Table,PDT)的物理地址由PDBR(Page Directory Base Register,也就是CR3寄存器)指定,每个任务都可以有自己的页目录。每个任务的TSS段就有自己的CR3的物理地址。每次进行任务切换的时候,CR3的内容都会被替换改为新任务的CR3域中的物理地址。页目录也是一个的页。但是他里面存放的是页表的地址,所以页目录可以指向1024个页表。

  页表也可以指向1024个页,每个项也是4个字节,同样页表的大小也是页的大小。

  页目录,页表和页的关系:(图片出处看水印)

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  处理器有页部件,专门负责线性地址到物理地址的转换工作。它首先将段部件送来的32位线性地址截成3段,分别是高10位(页目录的索引),中间的10位(页表的索引)和低12位(页内偏移)。操作系统要负责填写页目录和页表地址,然后程序的内存访问就可以像上图那样进行转换了。

2. 页目录、页表和CR3的填写

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  P(Present)是存在位,为“1”时,表示页表或者页位于内存中,否则,表示页表或者页不在内存中,必须先予以创建,或者从磁盘调入内存后方可使用。

  RW(Read/Write)是读/写位,为“0”时表示这样的页只能读取,为“1”时,可读可写。

  US(User/Supervisor)是用户/管理位。为“1”时,允许所有特权级别的程序访问;为“0”时,只允许特权级为0,1,2的程序访问,特权级为3的程序不能访问。

  PWT(Page-Level Write-Through)是页级通写位,和高速缓存有关,“通写”是处理器高速缓存的一种工作方式,这意味用来间接决定是否采用此种方式来改善页面的访问效率。

  A(Accessed)是访问位。该位由处理器固件来设置,用来指示此表项所指向的页是否被访问过。(这个坑爹的属性将会在练习题中体现)。

  D(Dirty)是脏位。该位由处理器固件来设置,用来指示此表项所指向的页是否被写入数据。(和A位一样,练习题会有体现)。

  PAT(Page Attribute Table)页属性表支持位。此位涉及更为复杂的分页系统。和页高速缓存有关。

  G(Global)是全局位,用来只是该表项指向的页是否为全局属性。如果页是全局属性的,那么,他将一直在高速缓存中一直保存(地址转换速度会加快)。因为页高速缓存的缓存容量有限,只能存放频繁使用的那些表项。而且,当因为任务切换等原因改变CR3寄存器的内容时,整个页高速缓存的内容都会被刷新。

  AVL位被处理器忽略,软件可以使用。

  进入分页模式之后,所有东西的地址都变成了虚拟线性地址了,包括GDT,LDT和TSS的地址等等。需要注意的是,页目录和页表在内存中的位置必须是在有效的可用内存范围(比如2GB内存只能在2GB内存中设置页目录和页表)。注意页目录必须进行初始化,主要是对P位置零,如果P位等于0,而处理器又执行了一个对这个页表的访问,那么就会引发异常中断。

清空页表以后就是要对页目录进行初始化了,在教材的“系统”中,把页目录设置在0x00020000这个物理地址中。然后在页目录中的最后一项,指向页目录自己。然后只进行最底下1MB内存的页面设置(所以只需要一个页表),然后就可以进行页表的初始化了,初始化过后。就可以直接进行CR3的设置,CR3的设置如下图:

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  其实CR3的设置和页表和页的填写差不多,只是除了PCD位和PWT位外,其他都不需要填写。

最后一步就是打开分页了,这个开关也是在CR0那里,称为PG位,在CR0的最高位31位上,注意这个功能只有在保护模式下才能开启(也就是CR0的PE位(0位)要是1),当PG位为1的时候,段部件传来的内存地址必须要经过页部件的转换才能得到真实的内存地址。

有关CR0-3这4个寄存器的控制可以看:http://www.cnblogs.com/Philip-Tell-Truth/articles/5341686.html

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  以上代码就就是开启页功能的一个演示。

3. 任务全局空间和局部空间的页面演示,页面查找的例子

在15章我们说过,任务的4GB地址空间包括两个部分:局部空间和全局空间,全局空间是所有任务共用的,内核就是所有任务共用的,它属于每个任务的全局空间。在教材的系统中,规定4GB的虚拟高2GB空间就是全局空间,地址范围是0x80000000-0xFFFFFFFF,局部空间使用低2GB空间,地址范围是0x00000000-0x7FFFFFFF,在任何时候,如果段部件发出的线性地址高于等于0x80000000,指向和访问的就是全局地址空间,或者说是内核。另外在上面我说过,我们把页目录的最后一项指向自己页目录的页,把页目录看成页表,再把页目录看成页表,刚好这个页表的最后一项也是指向页目录,那样我们就可以实现对页目录的内容进行访问,否则我们将无法访问页目录,因为处理器不允许访问一个没有登记的页(访问了会引发异常中断)。

但是为了保证内核的全局部分可以被其他程序使用,要创建内核的映射,内核应该具有两个部分,一个是和他的物理地址一样的对应(低地址)部分,另一个是映射到高地址的对应部分。这样说可能会比较抽象,我们直接看代码就好了。然后接下来就是把GDT的内容全部重定位了(GDT页必须使用虚拟线性地址,已经在前面把内核映射到高地址空间了),当然我们这里的全局空间映射位置取的比较好,是0x80000000,所以只要往GDT的最高4位or一个8就可以了。

ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配   

  注意 mov dword[es:ebx+esi],PDT_Mem_Address,add dword[es:ebx+esi],0x00001003这两句话,ebx的内容是0XFFFFF000,ESI的内容是0x00000200,因此段部件发出的线性地址是0XFFFFF200,现在举这个访问页目录的例子,以更加清楚了解如何访问页:

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  所以现在位于物理地址0x00021000这个地方的页被页目录中两个项所指向,但是这两个项所映射的物理地址时不一样的!!!!,页目录的第0项对应的映射是0x00000000-0x000FFFFF,0x800项对应的是0x80000000-0x800FFFFF。这也是一个分级管理有效地缩减表的占用的一个证据:因为全局空间总是映射到高区间,所以如果采用单映射的话,必须先准备2MB的表项先把局部空间描述完了才能到全局空间。

  注意因为段部件的内容不会因为你做好了映射而自动把自己的内容改变,所以需要显式切换,那就是上面代码的后面的部分。

★PART2:保护模式页管理模式下的内核任务的创建

1. 内核的虚拟内存的分配

  说实话教材说了那么多,其实就是最重要的是讲明白一个东西就行了,那就是页的分配。把这个搞明白了其他东西都是一个套路。

  现代操作系统可以跟踪所有页的分配状况。内存空间来自于插在主板上的内存条,按照新的工业标准,每个内存条上焊有一个很小的只读存储器,用于标明该内存条的容量和工作参数,作为一个PCI(E)设备,软件可以读取它,以获得计算机上的物理内存容量。然后简历上述的页分配表。但是由固件创建的表是每个表项占1个字节的,如果有4GB内存,则最多分220个页,要占用1MB的内存。但是如果以比特来指示页的使用状况,那么最多使用1048576个比特(128KB),将会产生巨大的内存节约。我们的系统为了简单,假定我们的系统只有2MB的内存,2MB的内存,可以分512个页,需要512个比特,我们可以在内核区定义比特串:

  ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  我们可以看到底下的1MB已经被内核和ROM-BIOS分配的差不多了。但是页的分配可以不连续的,接下来我们就可以在代码中看到这个问题。

ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

注意代码中我们使用了bts指令,这个指令用于测试串中的某个比特位。用该比特的值设置EFLAGS寄存器的CF标志位,然后将这个标志置“1”,他的最基本两种形式为:

bts r/m16,r16

bts r/m32,r32

  目的操作数可以是16/32位的通用寄存器,或者指向一个包含了16/32位实际操作数的内存党员,用于指定位串;源操作数可以是16/32位的通用寄存器,用于指定待测试的比特在位串的索引 (位置)。其他类似的指令还有:btr,btc和bt,他们的区别如下:

ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  例程首先做的事情就是在页目录看一下相应的页表是否已经在页目录中登记了,如果没有登记,则分配一个新的页作为页表然后写入页目录相应位置,否则就直接使用这个页表(当然这个管理程序非常粗糙,会有非常严重的问题,但是现在我们要把事情简单化)。

2. 任务程序的加载

  其实也是一堆套路,只不过我们需要注意的是,因为我们现在用的是页管理模式,所以要使用平坦模式。平坦模式是现代操作系统流行的管理模式,抛弃段管理模式冗杂的段的管理,可以大大简化代码的编写难度(注意的是页也有一点特权控制)。

  教材上加载程序的思路其实和前面几章是一样的,但是使用的是平坦模式来加载,并且对TCB进行了一些改变,而且使用了向上拓展的栈段(其实向上拓展的栈段只是段界限的检查会不一样,那是处理器的事情,push和pop指令的操作是一样的)。TCB变成了下图这个样子:

ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配

  最后要说明的是,分页机制下,和分段机制是一样的,内存也是先登记后使用的。程序在编写和编译之后,都是连续的,在加载后不能保证这一点。页的分配是随机的,尽管页不是连续的,但是线性地址是连续的就足够了。处理器访问数据,取指令,用的是线性地址。教材把所有的用户程序都从0x00000000开始加载,其实是不合理的,多段模型之下段内元素的偏移量都是相对于段而言的,在程序加载之后,段的描述符的基地址,就是段实际加载的位置。也就是这样,多段模型下,不管段加载到哪里,都不会影响段内元素的访问,这就是多段模型下程序可以重定位和浮动的根本原因。

  而在平坦模式下,程序的重定位和浮动实现比较复杂,在现代流行的操作系统中,编写的程序必须符合一定的规范才能重定位,比如很多系统要求用户提供一个标准的重定位表,列出所有需要动态加载的元素,程序加载后,操作系统会找到这个表,用实际的加载地址修正每一个表项。(非常复杂的一个过程)。

★PART3:本章的课后练习题

1. 显示当前任务的当前任务的虚拟地址的前55个双字,前50个页面的物理地址

显示虚拟地址前55个双字书上是有例程的,就是物理地址这个比较麻烦一点,其实也不麻烦,主要是要把页目录指向也目录自己和指向内核的那两个页表项修改为特权级3的程序也能访问。说下几个坑:

1. 之前几章的PrintDword过程都是有问题的,因为我忽略了我的put_char过程和书上的不一样,要把put_char压栈的那几个操作改成pushad,出栈改为popad

2. 页目录/页表内登记的物理地址的低12位都是有含义的!页表/页的物理地址的31-12位才会出现在项中,低12位是属性!特别是A和D位,处理器会自动将他们设置。显示物理内存的时候低12位都是0(and一下,页都是4KB对齐的)。

 ;===============================内核程序=================================
;定义内核所要用到的选择子
All_4GB_Segment equ 0x0008 ;4GB的全内存区域
Stack_Segement equ 0x0018 ;内核栈区
Print_Segement equ 0x0020 ;显存映射区
Sys_Routine_Segement equ 0x0028 ;公用例程段
Core_Data_Segement equ 0x0030 ;内核数据区
Core_Code_Segement equ 0x0038 ;内核代码段
;----------------------------------------------------------------
User_Program_AddressA equ ;用户程序所在逻辑扇区
User_Program_AddressB equ ;用户程序所在逻辑扇区
Switch_Stack_Size equ ;切换栈段的大小
PDT_Mem_Address equ 0x00020000 ;PDT在内存加载的地址
Global_Page_Directory equ 0x80000000 ;给全局空间的映射地址
;=============================内核程序头部===============================
SECTION header vstart=
Program_Length dd Program_end ;内核总长度
Sys_Routine_Seg dd section.Sys_Routine.start ;公用例程段线性地址
Core_Data_Seg dd section.Core_Data.start ;内核数据区线性地址
Core_Code_Seg dd section.Core_Code.start ;内核代码区线性地址
Code_Entry dd start ;注意偏移地址一定是32位的
dw Core_Code_Segement
;----------------------------------------------------------------
[bits ]
;=========================================================================
;============================公用例程区===================================
;=========================================================================
SECTION Sys_Routine align= vstart=
ReadHarddisk: ;push1:28位磁盘号(esi)
;push2:应用程序数据段选择子(ax->ds)
;push3: 偏移地址(ebx)
;push4: 应用程序代码段选择子(dx)
pushad
push ds
push es mov ebp,esp mov esi,[ebp+*]
movzx eax,word[ebp+*]
mov ebx,[ebp+*]
movzx edx,word[ebp+*] arpl ax,dx
mov ds,ax mov dx,0x1f2
mov al,0x01 ;读一个扇区
out dx,al inc edx ;0-7位
mov eax,esi
out dx,al inc edx ;8-15位
mov al,ah
out dx,al inc edx ;16-23位
shr eax,
out dx,al inc edx ;24-28位,主硬盘,LBA模式
mov al,ah
and al,0x0f
or al,0xe0
out dx,al inc edx
mov al,0x20
out dx,al _wait:
in al,dx
and al,0x88
cmp al,0x08
jne _wait mov dx,0x1f0
mov ecx,
_read:
in ax,dx
mov [ebx],ax
add ebx,
loop _read pop es
pop ds
popad
retf ;4个数据
;----------------------------------------------------------------
put_string: ;ebx:偏移地址
pushad
push ds
push es _print:
mov cl,[ebx]
cmp cl,
je _exit
call put_char
inc ebx
jmp _print
_exit:
pop es
pop ds
popad
retf ;段间返回
;--------------------------------------------------------------
put_char: ;cl就是要显示的字符
pushad
push es
push ds mov dx,0x3d4
mov al,0x0e ;高8位
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al ;先把高8位存起来
mov dx,0x3d4
mov al,0x0f ;低8位
out dx,al
mov dx,0x3d5
in al,dx ;现在ax就是当前光标的位置 _judge:
cmp cl,0x0a
je _set_0x0a
cmp cl,0x0d
je _set_0x0d
_print_visible:
mov bx,ax
mov eax,Print_Segement
mov es,eax
shl bx, ;注意这里一定要把ebx变成原来的两倍,实际位置是光标位置的两倍
mov [es:bx],cl ;注意这里是屏幕!
mov byte[es:bx+],0x07
add bx,
shr bx,
jmp _roll_screen
_set_0x0d: ;回车
mov bl,
div bl
mul bl
mov bx,ax
jmp _set_cursor
_set_0x0a: ;换行
mov bx,ax
add bx,
jmp _roll_screen
_roll_screen:
cmp bx,
jl _set_cursor
mov eax,Print_Segement
mov ds,eax
mov es,eax cld
mov edi,0x00
mov esi,0xa0
mov ecx,
rep movsw
_cls:
mov bx,
mov ecx,
_print_blank:
mov word[es:bx],0x0720
add bx,
loop _print_blank
mov bx, ;别总是忘了光标的位置!
_set_cursor: ;改变后的光标位置在bx上
mov dx,0x3d4
mov al,0x0f ;低8位
out dx,al mov al,bl
mov dx,0x3d5
out dx,al mov dx,0x3d4
mov al,0x0e ;高8位
out dx,al mov al,bh
mov dx,0x3d5
out dx,al pop ds
pop es
popad
ret
;----------------------------------------------------------------
Make_Seg_Descriptor: ;构造段描述符
;输入:
;eax:线性基地址
;ebx:段界限
;ecx:属性
;输出:
;eax:段描述符低32位
;edx:段描述符高32位
mov edx,eax
and edx,0xffff0000
rol edx,
bswap edx
or edx,ecx shl eax,
or ax,bx
and ebx,0x000f0000
or edx,ebx
retf
;----------------------------------------------------------------
Make_Gate_Descriptor: ;构造门描述符
;输入:
;eax:段内偏移地址
;bx: 段的选择子
;cx: 段的属性
;输出:
;eax:门描述符低32位
;edx:门描述符高32位
push ebx
push ecx mov edx,eax
and edx,0xffff0000 ;要高16位
or dx,cx shl ebx,
and eax,0x0000ffff
or eax,ebx pop ecx
pop ebx retf
;----------------------------------------------------------------
Set_New_GDT: ;装载新的全局描述符
;输入:edx:eax描述符
;输出:cx选择子
push ds
push es mov ebx,Core_Data_Segement
mov ds,ebx mov ebx,All_4GB_Segment
mov es,ebx sgdt [pgdt_base_tmp] movzx ebx,word[pgdt_base_tmp]
inc bx ;注意这里要一定是inc bx而不是inc ebx,因为gdt段界限初始化是0xffff的
;要用到回绕特性
add ebx,[pgdt_base_tmp+0x02] ;得到pgdt的线性基地址 mov [es:ebx],eax
mov [es:ebx+0x04],edx ;装载新的gdt符
;装载描述符要装载到实际位置上 add word[pgdt_base_tmp], ;给gdt的段界限加上8(字节) lgdt [pgdt_base_tmp] ;加载gdt到gdtr的位置和实际表的位置无关 mov ax,[pgdt_base_tmp] ;得到段界限
xor dx,dx
mov bx, ;得到gdt大小
div bx
mov cx,ax
shl cx, ;得到选择子,ti=0(全局描述符),rpl=0(申请特权0级) pop es
pop ds
retf
;----------------------------------------------------------------
Set_New_LDT_To_TCB: ;装载新的局部描述符
;输入:edx:eax描述符
; : ebx:TCB线性基地址
;输出:cx选择子 push edi
push eax
push ebx
push edx
push ds mov ecx,All_4GB_Segment
mov ds,ecx mov edi,[ebx+0x0c] ;LDT的线性基地址
movzx ecx,word[ebx+0x0a]
inc cx ;得到实际的LDT的大小(界限还要-1) mov [edi+ecx+0x00],eax
mov [edi+ecx+0x04],edx add cx,
dec cx mov [ebx+0x0a],cx mov ax,cx
xor dx,dx
mov cx,
div cx shl ax,
mov cx,ax
or cx,0x0004 ;LDT,第三位TI位一定是1 pop ds
pop edx
pop ebx
pop eax
pop edi
retf
;----------------------------------------------------------------
PrintDword: ;显示edx内容的一个调试函数
pushad
push ds mov eax,Core_Data_Segement
mov ds,eax mov ebx,bin_hex
mov ecx, _query:
rol edx,
mov eax,edx
and eax,0x0000000f
xlat push ecx
mov cl,al
call put_char
pop ecx loop _query pop ds
popad retf
;----------------------------------------------------------------
allocate_4KB_page: ;输入:无
;输出eax:页的物理地址
;注意这个是近调用
push ebx
push ecx
push edx
push ds mov eax,Core_Data_Segement
mov ds,eax xor eax,eax _search_pages:
bts [page_bit_map],eax
jnc _found_not_uesd
inc eax
cmp eax,page_map_len*
jl _search_pages mov ebx,No_More_Page
call Sys_Routine_Segement:put_string
hlt ;无可用页,直接停机 _found_not_uesd:
shl eax, ;eax相当于是选择子,乘以一个4KB得到物理地址 pop ds
pop edx
pop ecx
pop ebx
ret
;----------------------------------------------------------------
alloc_inst_a_page: ;分配一个页,并安装在当前活动的层级分页结构中
;输入:EBX=页的线性地址
;输出:无
push eax
push ebx
push edi
push esi
push ds mov eax,All_4GB_Segment ;转平坦模式
mov ds,eax _test_P: ;在页目录中看是否存在这个页表
mov esi,ebx
and esi,0xffc00000
shr esi,
or esi,0xfffff000 ;指向页目录本身
test dword[esi],0x00000001
jnz _get_page_and_create_new_page
_create_new_page_directory:
call allocate_4KB_page
or eax,0x00000007 ;存在于主存,可读可写,允许特权级3程序访问
mov [esi],eax
_get_page_and_create_new_page:
mov esi,ebx
shr esi, ;页表在页目录的偏移项
and esi,0x003ff000 ;得到页表的偏移地址
or esi,0xffc00000 ;指向页目录 and ebx,0x003ff000
shr ebx, ;中间10位是页目录-页表-表内偏移量(注意这里的层次理解)
or esi,ebx ;esi就是页的对应线性地址
call allocate_4KB_page
or eax,0x00000007 ;存在于主存,可读可写,允许特权级3程序访问
mov [esi],eax pop ds
pop esi
pop edi
pop ebx
pop eax
retf
;----------------------------------------------------------------
Copy_Page: ;把在创建的包含全局和私有部分的页表复制一份给用户程序用
;输入:无
;输出eax:页的物理地址
push ds
push es
push edi
push esi
push ebx
push ecx
push edx mov eax,Core_Data_Segement
mov ds,eax
mov edx,[task_pos]
sub edx,
add edx,[page_header]
mov edi,[page_soft_header]
sub edi,0x1000
mov esi,[page_header] ;指向全局页目录
mov [es:0x16],edi mov eax,All_4GB_Segment
mov ds,eax
mov es,eax call allocate_4KB_page
mov ebx,eax
or ebx,0x00000007
mov [edx],ebx mov ecx,
cld
repe movsd pop edx
pop ecx
pop ebx
pop esi
pop edi
pop es
pop ds retf
;----------------------------------------------------------------
;=========================================================================
;===========================内核数据区====================================
;=========================================================================
SECTION Core_Data align= vstart=
;-------------------------------------------------------------------------------
pgdt_base_tmp: dw
dd salt:
salt_1: db '@Printf' ;@Printf函数(公用例程)
times -($-salt_1) db
dd put_string
dw Sys_Routine_Segement
dw ;参数个数 salt_2: db '@ReadHarddisk' ;@ReadHarddisk函数(公用例程)
times -($-salt_2) db
dd ReadHarddisk
dw Sys_Routine_Segement
dw ;参数个数 salt_3: db '@PrintDwordAsHexString' ;@PrintDwordAsHexString函数(公用例程)
times -($-salt_3) db
dd PrintDword
dw Sys_Routine_Segement
dw ;参数个数 salt_length: equ $-salt_3
salt_items_sum equ ($-salt)/salt_length ;得到项目总数 salt_tp: dw ;任务门,专门拿来给程序切换到全局空间的 message_1 db ' If you seen this message,that means we '
db 'are now in protect mode,and the system '
db 'core is loaded,and the video display '
db 'routine works perfectly.',0x0d,0x0a, message_2 db ' Loading user program...', do_status db 'Done.',0x0d,0x0a, message_3 db 0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
db ' User program terminated,control returned.'
db 0x0d,0x0a,0x0d,0x0a,
message_4 db ' We have been backed to kernel.',0x0d,0x0a,
message_5 db ' The GDT and memory have benn recycled.',
message_6 db ' From the system wide gate:',0x0d,0x0a,
message_7 db ' Setting the gate discriptor...',
message_In_Gate db ' Hi!My name is Philip:',0x0d,0x0a,
message_page db ' Paging is enabled.System core is mapped to'
db ' address 0x80000000.',0x0d,0x0a,
core_stop db 0x0d,0x0a,' Processor HALT.',
No_More_Page db '********No more pages********',
task_switch db 0x0d,0x0a,' Task switching...@_@',0x0d,0x0a, bin_hex db '0123456789ABCDEF'
;put_hex_dword子过程用的查找表
core_buf times db ;内核用的缓冲区(2049个字节(2MB)) esp_pointer dd ;内核用来临时保存自己的栈指针 cpu_brnd0 db 0x0d,0x0a,' ',
cpu_brand times db
cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,
core_ss dw
core_sp dd
;程序管理器的任务信息
prgman_tss dd ;程序管理器的TSS基地址
dw ;程序管理器的TSS描述符选择子
;假设只有2MB内存可以用的意思,正确的做法应该先读PCI(E),然后再分配!
page_bit_map db 0xff,0xff,0xff,0xff,0xff,0x55,0x55,0xff
db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
db 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
page_map_len equ $-page_bit_map
core_next_laddr dd 0x80100000 ;内核空间中下一个可分配的线性地址
task_pos dd 0x00000ffc ;任务程序的页表在全局页目录的偏移
page_header dd 0xfffff000 ;全局页目录
page_soft_header dd 0xfffff000 ;加载页目录地址 tcb_chain dd ;任务控制块链头指针
;=========================================================================
;===========================内核代码区====================================
;=========================================================================
SECTION Core_Code align= vstart=
;---------------------------------------------------------------------
append_to_tcb: ;写入新的TCB链
;输入:ecx新的TCB线性基地址
pushad push ds
push es mov eax,All_4GB_Segment
mov es,eax mov eax,Core_Data_Segement
mov ds,eax mov dword[es:ecx+0x00],
mov eax,[tcb_chain]
cmp eax,0x00
je _notcb _search_tcb:
mov edx,[tcb_chain+0x00]
mov eax,[es:edx]
cmp eax,0x00
jne _search_tcb mov [es:edx+0x00],ecx
jmp _out_tcb_search _notcb:
mov [tcb_chain],ecx _out_tcb_search:
pop es
pop ds popad
ret
;---------------------------------------------------------------------
load_program: ;输入push1:逻辑扇区号
; push2: 线性基地址
pushad
push ds
push es mov ebp,esp ;别忘了把参数传给ebp mov eax,Core_Data_Segement
mov ds,eax ;切换到内核数据段 mov eax,All_4GB_Segment
mov es,eax mov ebx,0xfffff000
xor esi,esi
_flush_private:
mov dword[es:ebx+esi*],0x00000000
inc esi
cmp esi,
jl _flush_private mov edi,[ebp+*] ;获取tcb的线性基地址,别忘了调用相对近调用还要有1个push
mov esi,[ebp+*] ;esi必须是逻辑扇区号
mov ebx,core_buf ;ebx要在内核数据缓冲区(先读取头部在缓冲区,esi已经是有扇区号了) push esi
push ds
push ebx
push cs
call Sys_Routine_Segement:ReadHarddisk mov eax,[core_buf] ;读取用户程序长度
mov ebx,eax
and ebx,0xfffff000 ;清空低12位(强制对齐4096:4KB)
add ebx,
test eax,0x00000fff
cmovnz eax,ebx ;低12位不为0则使用向上取整的结果 mov ecx,eax
shr ecx, ;看占了几页
mov eax,All_4GB_Segment ;切换到4GB段区域(平坦模式)
mov ds,eax
mov edi,[ebp+*] ;获取tcb的线性基地址
mov esi,[ebp+*] ;esi必须是逻辑扇区号 _loop_read_@1:
mov ebx,[es:edi+0x06] ;从TCB取得下一个可用的虚拟线性地址
add dword[es:edi+0x06],0x1000
call Sys_Routine_Segement:alloc_inst_a_page push ecx
mov ecx, ;512*8==4096
_loop_read_@2:
push esi
push ds
push ebx
push cs
call Sys_Routine_Segement:ReadHarddisk ;esi还是User_Program_Address mov eax,[es:0xffc00000]
mov eax,[es:0xffc00004]
mov eax,[es:0xffc00008] inc esi
add ebx,
loop _loop_read_@2
pop ecx
loop _loop_read_@1 mov eax,Core_Data_Segement ;把数据段切回来
mov ds,eax
mov esi,edi ;esi: TCB的线性基地址 mov ebx,[core_next_laddr] ;TSS必须在全局空间中进行
call Sys_Routine_Segement:alloc_inst_a_page
add dword[core_next_laddr], mov [es:esi+0x14],ebx ;填写TSS的线性基地址
mov word[es:esi+0x12], ;无I/O映射 mov ebx,[es:esi+0x06] ;从用户私有空间建立LDT
add dword[es:esi+0x06],0x1000
call Sys_Routine_Segement:alloc_inst_a_page
mov [es:esi+0x0c],ebx ;填写LDT的线地址 mov edi,[es:esi+0x14] ;edi就是TSS的线性地址
;接下来就是一堆套路了,这里和15章14章的程序不一样,这里直接填TSS,比较直观
;不用给用户程序头回填段的选择子了,现在是平坦模式的演示
;代码段
mov eax,0x00000000
mov ebx,0x000fffff
mov ecx,0x00c0f800
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0003 ;特权级为3
mov [es:edi+],cx ;CS域 ;数据段
mov eax,0x00000000
mov ebx,0x000fffff
mov ecx,0x00c0f200
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0003 ;特权级为3
mov [es:edi+],cx ;ES域,已经映射到全局空间了
mov [es:edi+],cx ;DS域,已经映射到全局空间了
mov [es:edi+],cx ;FS域,已经映射到全局空间了
mov [es:edi+],cx ;GS域,已经映射到全局空间了 ;创建一系列栈
;创建自身特权级为3的栈
mov ebx,[es:esi+0x06]
add dword[es:esi+0x06],0x1000
call Sys_Routine_Segement:alloc_inst_a_page ;自动在用户私有空间的页表登记了
mov [es:edi+],cx ;cx是数据段的选择子
mov edx,[es:esi+0x06] ;向上拓展的ESP的初始值
mov [es:edi+],edx ;创建特权级0的栈
mov ebx,[es:esi+0x06]
add dword[es:esi+0x06],0x1000
call Sys_Routine_Segement:alloc_inst_a_page ;自动在用户私有空间的页表登记了 mov eax,0x00000000
mov ebx,0x000fffff
mov ecx,0x00c09200 ;4KB粒度的堆栈段描述符,特权级0
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0000 ;选择子特权级为0 mov [es:edi+],cx ;cx是数据段的选择子
mov edx,[es:esi+0x06] ;向上拓展的ESP0的初始值
mov [es:edi+],edx ;创建特权级1的栈
mov ebx,[es:esi+0x06]
add dword[es:esi+0x06],0x1000
call Sys_Routine_Segement:alloc_inst_a_page ;自动在用户私有空间的页表登记了 mov eax,0x00000000
mov ebx,0x000fffff
mov ecx,0x00c0b200 ;4KB粒度的堆栈段描述符,特权级1
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0001 ;选择子特权级为1 mov [es:edi+],cx ;cx是数据段的选择子
mov edx,[es:esi+0x06] ;向上拓展的ESP1的初始值
mov [es:edi+],edx ;创建特权级2的栈
mov ebx,[es:esi+0x06]
add dword[es:esi+0x06],0x1000
call Sys_Routine_Segement:alloc_inst_a_page ;自动在用户私有空间的页表登记了 mov eax,0x00000000
mov ebx,0x000fffff
mov ecx,0x00c0d200 ;4KB粒度的堆栈段描述符,特权级2
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0002 ;选择子特权级为2 mov [es:edi+],cx ;cx是数据段的选择子
mov edx,[es:esi+0x06] ;向上拓展的ESP2的初始值
mov [es:edi+],edx ;现在开始重定位API符号表
;---------------------------------------------------------------------
mov eax,All_4GB_Segment ;因为这个时候用户头部在LDT,而LDT还没有被加载,只能通过4GB空间访问
mov es,eax
mov eax,Core_Data_Segement
mov ds,eax cld
mov ecx,[es:0x0c]
mov edi,[es:0x08] _loop_U_SALT:
push edi
push ecx mov ecx,salt_items_sum
mov esi,salt _loop_C_SALT:
push edi
push esi
push ecx mov ecx, ;比较256个字节
repe cmpsd
jne _re_match ;如果成功匹配,那么esi和edi刚好会在数据区之后的 mov eax,[esi] ;偏移地址
mov [es:edi-],eax ;把偏移地址填入用户程序的符号区
mov ax,[esi+0x04] ;段的选择子 or ax,0x0002 ;把RPL改为3,代表(内核)赋予应用程序以特权级3
mov [es:edi-],ax ;把段的选择子填入用户程序的段选择区 _re_match:
pop ecx
pop esi
add esi,salt_length
pop edi
loop _loop_C_SALT pop ecx
pop edi
add edi,
loop _loop_U_SALT
;---------------------------------------------------------------------
;----------------------填入临时中转任务门选择子-----------------------
mov ax,[salt_tp]
mov [es:0x14],ax ;填充任务门选择子
;---------------------------------------------------------------------
mov esi,[ebp+*] ;重新获得TCB的线性基地址 ;在GDT中存入LDT信息
mov eax,[es:esi+0x0c]
movzx ebx,word[es:esi+0x0a]
mov ecx,0x00408200 ;LDT描述符,特权级0级
call Sys_Routine_Segement:Make_Seg_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [es:esi+0x10],cx ;在TCB放入LDT选择子 ;构建TSS剩下的信息表
mov ebx,[es:esi+0x14]
mov [es:ebx+],cx ;TSS中LDT选择子 mov word[es:ebx+], ;填充反向链(任务切换的时候处理器会帮着填的,不用操心)
mov dx,[es:esi+0x12] ;TSS段界限
mov [es:ebx+],dx
mov word[es:ebx+], ;T=0 mov eax,[es:0x04] ;从任务的4GB地址空间获取入口点
mov [es:ebx+],eax ;填写TSS的EIP域 pushfd
pop edx
mov [es:ebx+],edx ;EFLAGS ;在GDT中存入TSS信息
mov eax,[es:esi+0x14]
movzx ebx,word[es:esi+0x12]
mov ecx,0x00408900
call Sys_Routine_Segement:Make_Seg_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [es:esi+0x18],cx ;复制一份页表
call Sys_Routine_Segement:Copy_Page
mov ebx,[es:esi+0x14]
mov [es:ebx+],eax ;填写PDBR(CR3) pop es
pop ds
popad
ret ;相当于是stdcall,过程清栈
;---------------------------------------------------------------------
start:
mov eax,Core_Data_Segement
mov ds,eax
mov eax,All_4GB_Segment
mov es,eax mov ebx,message_1
call Sys_Routine_Segement:put_string ;下面准备开启页管理
mov ecx,
mov ebx,PDT_Mem_Address
xor esi,esi _flush_PDT: ;清空页表
mov dword[es:ebx+esi*],0x00000000
inc esi
loop _flush_PDT ;下面的代码会非常绕,注意观察
;页目录的最后一个32字节是指向自己的页表(这个页表就是页目录)
mov dword[es:ebx+],PDT_Mem_Address
or dword[es:ebx+],0x00000007 ;属性:存在于物理内存,可写可读,啥程序都能访问 ;页目录的第一个页表指示最底下1MB内存的4KB页(内核代码,必须虚拟地址和物理地址一致)
mov dword[es:ebx+],PDT_Mem_Address
or dword[es:ebx+],0x00000007 ;属性:存在于物理内存,可写可读,啥程序都能访问
or dword[es:ebx+],0x00001000 ;注意是这个页表是放在目录的后一个页! ;现在0x00020000的页是第0个页表(指示页目录),0x00021000是第一个页表(指示底下1MB的东西)
mov ebx,PDT_Mem_Address
or ebx,0x00001000
xor eax,eax
xor esi,esi _make_page:
mov edx,eax
or edx,0x00000007 ;属性:存在于物理内存,可写可读,啥程序都能访问
mov [es:ebx+esi*],edx ;物理位置
add eax,0x1000
inc esi
cmp esi,
jl _make_page _make_page_last:
mov dword[es:ebx+esi*],0x00000000 ;标记页为无效
inc esi
cmp esi,
jl _make_page_last mov eax,PDT_Mem_Address
mov cr3,eax ;把页目录基地址放在cr3,准备开启页功能 mov eax,cr0
or eax,0x80000000
mov cr0,eax ;置PG位,开启页功能 ;--------------------------已开启页功能--------------------------------
;------------------开始映射高端内存区到页目录表中----------------------
mov ebx,0xfffff000 ;表示页表指向页目录自己
mov esi,Global_Page_Directory ;映射的起始地址
shr esi,
shl esi,
mov dword[es:ebx+esi],PDT_Mem_Address
add dword[es:ebx+esi],0x00001003 sgdt [pgdt_base_tmp]
mov ebx,[pgdt_base_tmp+] ;GDT线性基地址 or dword[es:ebx+0x10+],Global_Page_Directory ;0x10是刚好是64个字节,忽略0字串
or dword[es:ebx+0x18+],Global_Page_Directory
or dword[es:ebx+0x20+],Global_Page_Directory
or dword[es:ebx+0x28+],Global_Page_Directory
or dword[es:ebx+0x30+],Global_Page_Directory
or dword[es:ebx+0x38+],Global_Page_Directory add dword[pgdt_base_tmp+],Global_Page_Directory ;线性基地址也要变 lgdt [pgdt_base_tmp]
jmp Core_Code_Segement:_flush ;强制刷新代码段,映射到高端内存区 _flush:
mov eax,Stack_Segement
mov ss,eax mov eax,Core_Data_Segement
mov ds,eax mov ebx,message_page
call Sys_Routine_Segement:put_string
;---------------------------------------------------------------------- _@load:
;----------------------------安装门------------------------------------
mov edi,salt
mov ecx,salt_items_sum
_set_gate:
push ecx
mov eax,[edi+]
mov bx,[edi+] ;选择子
mov cx,0xec00 ;门是特权级是3的门,那么任何程序都能调用
or cx,[edi+] ;加上参数个数 call Sys_Routine_Segement:Make_Gate_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [edi+],cx ;回填选择子
add edi,salt_length
pop ecx
loop _set_gate
;----------------------------------------------------------------------
mov ebx,message_In_Gate
call far [salt_1+] ;调用门显示字符信息(忽略偏移地址(前4字节)) mov ebx,[core_next_laddr]
call Sys_Routine_Segement:alloc_inst_a_page
add dword[core_next_laddr], ;指向下一个页 mov word[es:ebx+], ;TI=0
mov word[es:ebx+], ;任务管理器不需要I/O映射,要大于等于界限
mov word[es:ebx+], ;任务允许没有自己的LDT
mov eax,cr3
mov dword[es:ebx+],eax ;设置CR3,注意不是0了!
mov word[es:ebx+], ;没有前一个任务 mov eax,ebx
mov ebx, ;TSS段界限
mov ecx,0x00408900
call Sys_Routine_Segement:Make_Seg_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [prgman_tss+0x04],cx ltr cx ;启动任务
;------------------安装用户管理程序的临时返回任务门--------------------
mov eax,0x0000 ;TSS不需要偏移地址
mov bx,[prgman_tss+0x04] ;TSS的选择子
mov cx,0xe500 call Sys_Routine_Segement:Make_Gate_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [salt_tp],cx ;填入临时中转任务门选择子,注意不需要加260了
;----------------------------------------------------------------------
mov ebx,[core_next_laddr]
call Sys_Routine_Segement:alloc_inst_a_page
add dword[core_next_laddr], ;指向下一个页 mov dword[es:ebx+0x06], ;用户程序从0位置开始分配
mov word[es:ebx+0x0a],0xffff ;LDT初始界限
mov ecx,ebx ;添加到TCB链中
call append_to_tcb push dword User_Program_AddressA
push ecx call load_program mov ebx,task_switch
call Sys_Routine_Segement:put_string jmp far [es:ecx+0x14]
;call far [es:ecx+0x14] mov ebx,core_stop
call Sys_Routine_Segement:put_string hlt
;----------------------------------------------------------------------
;=========================================================================
SECTION core_trail
;----------------------------------------------------------------
Program_end:
 ;================================用户程序=======================================
program_length dd program_end ;程序总长度#0x00
entry_point dd start ;程序入口点#0x04
salt_position dd salt_begin ;SALT表起始偏移量#0x08
salt_items dd (salt_end-salt_begin)/
;SALT条目数#0x0C
TpBack: dd ;任务门的偏移地址没用,直接填充就可以了
dw ;任务门的选择子#0x14
Own_Page dd ;自己页面的物理地址#0x16
;-------------------------------------------------------------------------------
;符号地址检索表
salt_begin:
PrintString db '@Printf'
times -($-PrintString) db
TerminateProgram: db '@TerminateProgram'
times -($-TerminateProgram) db
;-------------------------------------------------------------------------------
reserved times * db ;保留一个空白区,以演示分页
;-------------------------------------------------------------------------------
ReadDiskData db '@ReadHarddisk'
times -($-ReadDiskData) db
PrintDwordAsHex db '@PrintDwordAsHexString'
times -($-PrintDwordAsHex) db
salt_end:
message_0 db 0x0d,0x0a,
db ' ............User task is running with '
db 'paging enabled!............',0x0d,0x0a,
message_1 db 0x0d,0x0a,
db ' ..........,,,..The address of the user'
db ' task(first 50)!...........',0x0d,0x0a,
space db 0x20,0x20,
next_line: db 0x0d,0x0a,
Own_Page_Message db 0x0d,0x0a,
db 0x20,0x20,'Task Page: ',
;-------------------------------------------------------------------------------
[bits ]
;-------------------------------------------------------------------------------
start:
;--------------------------显示50个页内容------------------------
mov ebx,message_0
call far[PrintString] xor esi,esi
mov ecx,
.b1:
mov ebx,space
call far[PrintString] mov edx,[esi*]
call far[PrintDwordAsHex]
inc esi
loop .b1 ;--------------------显示新任务页目录地址--------------------------
mov ebx,Own_Page_Message
call far[PrintString]
mov ebx,space
call far [PrintString]
mov ebx,[Own_Page]
mov edx,[es:ebx]
and edx,0xfffff000 ;处理器会设置A和D位,直接忽略得到物理地址了
call far[PrintDwordAsHex] mov ebx,next_line
call far [PrintString]
;--------------------显示任务前50个页物理地址----------------------
mov ebx,message_1
call far[PrintString]
mov edi,0xffc00000
mov ecx,
xor esi,esi _show:
mov ebx,space
call far [PrintString] mov edx,[es:edi+esi*]
and edx,0xfffff000 ;处理器会设置A和D位,直接忽略得到物理地址了
call far[PrintDwordAsHex]
inc esi
loop _show
jmp far [fs:TpBack]
;iretd
;-------------------------------------------------------------------------------
program_end: