目录
一、前景回顾
二、规划页表
三、实现页表
四、运行测试
前面我们已经介绍了分页机制的运行原理,那么如何开启分页机制呢,也简单,分为如下三个步骤:
1、创建页目录表并初始化页内存。
2、将页目录表地址赋值为CR3。
3、打开CR0寄存器的PG位。
可以看出页表是分页机制的核心,接下来我们将开始在我们的系统上实现一个二级页表。
设计页表其实就是设计内存布局,不过在规划内存布局之前,我们需要了解用户进程与操作系统之间的关系。
在操作系统中,为了计算机安全,用户进程始终是运行在低特权级的。用户进程需要访问硬件相关资源时,是需要向操作系统申请,然后通过系统调用的方式陷入操作系统,由操作系统去做并且将结果返回给用户进程。进程可以有多个,但是操作系统只有一个。所以操作系统必须“共享”给每一个进程,他们的关系如图所示。

对于每一个进程来说,它不单单是运行程序这么简单的事,它需要接受调度、阻塞、在需要陷入内核时还需要操作系统的帮助等等,因此,完整的一个进程是需要和操作系统配合才能完成正常的工作。也就是说每一个进程里面应该包含操作系统部分。Linux下的每一个进程,高1GB的空间就是留给操作系统的,低3GB的空间就是留给进程用户空间自身的,在我们的系统中,也遵循这样的安排。对于这高1GB的空间,我们并不是每创建一个新的进程,就将操作系统代码给复制到这1GB的空间中,这样显得笨重而且随着进程数的增加会占用更多的内存。实际上操作系统的代码只有一份,我们每创建一个新的进程,就让该进程的高1GB空间指向操作系统即可。
现在回过头来看看我们的系统,这里提前透露一下,后面完善了内核之后,我们的整个操作系统的代码也不到1MB,所以我们这里就假设最终操作系统的代码只有1MB,也就是说整个操作系统代码的存放区域从0x0到0xFFFFF。跟前面说的划分1GB的空间来存放操作系统代码差距特别大,很大一部分没有用上,因为我们的操作系统会比较简单,用不到这么多空间。不过内存划分还是按照Linux下的格式来,便于学习。
所以页目录表的地址,我们就存放在物理地址0x100000处,为了让页表紧挨着页目录表,页目录表本身占用4KB,所以第一个页表的物理地址是0x101000,还有其他的规划我一并表现在它们的物理内存布局中,如图所示。

这张图将我们的整个页表规划表现得一览无余,至于其中细节且让我娓娓道来。
首先我们知道,页表是用来给虚拟地址映射用的,虚拟地址的使用前提得是开启了分页机制,如果没有开启分页机制,那么页表就毫无用处。现在假设我们开启了分页机制,那么此时注意,我们使用的地址就不再是之前的线性地址了,而是虚拟地址。怎么说呢,以前我们要想让CPU访问一个地址,只需要将该地址拆解分别赋给CS和段内偏移就可以了,但是开启分页机制后,拆解出的这个地址就不能访问到期望的地址了,因为在开启分页机制后,CPU拿到这个地址,会根据CR3寄存器中存储的页目录表地址来进行寻址,最终得到的物理地址才是CPU真正去访问的地址。具体步骤请参考上一回的内容。
所以现在如果我们想要访问操作系统的代码,也就是低端1MB的内存,该如何访问呢?前面我们说过,Linux将用户进程的高1GB作为操作系统的空间,所以我们可以知道,在用户进程中,虚拟地址0xc0000000~0xffffffff便是映射到操作系统的1GB空间中去,而在我们的系统中,操作系统的代码总共占据了1MB的内存,所以从0xc0000000~0xc00fffff便是映射到我们操作系统的1MB空间,0xc0000000虚拟地址对应的页目录项应该是第768个,这个算起来容易,0xc0000000的高10位是0x300,即十进制的768。该目录项可以表示的内存空间是4MB,所以我们指定了一个页表来管理这4MB的空间,因此我们在页目录表的第768页目录项中填入该页表的物理地址0x101000,我们将目光转入到0x101000地址处,此处页表的第0到255页表项指向的物理内存便是我们操作系统的1MB空间。
从页目录项的第769到1022的页目录项中,我们都只是指定了页表地址,并没有给实际的页表初始化,因为我们的操作系统只占用了1MB空间,多余的也用不上,这里只是为了占个位置而已。
页目录项的第1023项可能会有人比较好奇,为什么该项指向的地址是页目录表本身地址,这里是为了能通过虚拟地址来访问到页目录表本身,如果后面需要修改页目录表,我们通过0xfffff000~0xffffffff就能访问到页目录表的第0到第1023项,感兴趣的朋友可以自己试试,看这个虚拟地址最终是否能转换成页目录表各项的物理地址。当然可能就会有人说,这样的话操作系统实际上就并没有占据1GB的内存空间了么,少掉了4MB的空间,事实上的确是这样,不过其实问题也不大,是吧。
最后我们再来看看为什么页目录表的第0项的内容是0x101000。原因是我们在加载内核之前,程序中一直都是运行的loader,它本身的代码都是在低端1MB之内的,必须保证之前段机制下的线性地址和分页后的虚拟地址对应的物理地址一致。
前面说了这么多,接下来我们来实现我们的页目录表和页表。在loader.S文件中增加如下代码:
1 %include "boot.inc"
2 section loader vstart=LOADER_BASE_ADDR
3 LOADER_STACK_TOP equ LOADER_BASE_ADDR
4 jmp loader_start
5
6 ;构建gdt及其内部描述符
7 GDT_BASE: dd 0x00000000
8 dd 0x00000000
9 CODE_DESC: dd 0x0000FFFF
10 dd DESC_CODE_HIGH4
11 DATA_STACK_DESC: dd 0x0000FFFF
12 dd DESC_DATA_HIGH4
13 VIDEO_DESC: dd 0x80000007
14 dd DESC_VIDEO_HIGH4
15
16 GDT_SIZE equ $-GDT_BASE
17 GDT_LIMIT equ GDT_SIZE-1
18 times 60 dq 0 ;此处预留60个描述符的空位
19
20 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
21 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
22 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
23
24 ;以下是gdt指针,前2个字节是gdt界限,后4个字节是gdt的起始地址
25 gdt_ptr dw GDT_LIMIT
26 dd GDT_BASE
27
28 ;---------------------进入保护模式------------
29 loader_start:
30 ;一、打开A20地址线
31 in al, 0x92
32 or al, 0000_0010B
33 out 0x92, al
34
35 ;二、加载GDT
36 lgdt [gdt_ptr]
37
38 ;三、cr0第0位(pe)置1
39 mov eax, cr0
40 or eax, 0x00000001
41 mov cr0, eax
42
43 jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
44
45 [bits 32]
46 p_mode_start:
47 mov ax, SELECTOR_DATA
48 mov ds, ax
49 mov es, ax
50 mov ss, ax
51 mov esp, LOADER_STACK_TOP
52 mov ax, SELECTOR_VIDEO
53 mov gs, ax
54
55 mov byte [gs:160], 'p'
56
57 ;------------------开启分页机制-----------------
58 ;一、创建页目录表并初始化页内存位图
59 call setup_page
60
61 ;将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
62 sgdt [gdt_ptr]
63 ;将gdt描述符中视频段描述符中的段基址+0xc0000000
64 mov ebx, [gdt_ptr + 2]
65 or dword [ebx + 0x18 + 4], 0xc0000000
66
67 ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
68 add dword [gdt_ptr + 2], 0xc0000000
69
70 add esp, 0xc0000000 ;将栈指针同样映射到内核地址
71
72 ;二、将页目录表地址赋值给cr3
73 mov eax, PAGE_DIR_TABLE_POS
74 mov cr3, eax
75
76 ;三、打开cr0的pg位
77 mov eax, cr0
78 or eax, 0x80000000
79 mov cr0, eax
80
81 ;在开启分页后,用gdt新的地址重新加载
82 lgdt [gdt_ptr]
83 mov byte [gs:160], 'H'
84 mov byte [gs:162], 'E'
85 mov byte [gs:164], 'L'
86 mov byte [gs:166], 'L'
87 mov byte [gs:168], 'O'
88 mov byte [gs:170], ' '
89 mov byte [gs:172], 'P'
90 mov byte [gs:174], 'A'
91 mov byte [gs:176], 'G'
92 mov byte [gs:178], 'E'
93
94 jmp $
95 ;---------------------------------------------
96
97 ;--------------函数声明------------------------
98 ;setup_page:(功能)设置分页------------
99 setup_page:
100 ;先把页目录占用的空间逐字节清0
101 mov ecx, 4096
102 mov esi, 0
103 .clear_page_dir:
104 mov byte [PAGE_DIR_TABLE_POS + esi], 0
105 inc esi
106 loop .clear_page_dir
107
108 ;开始创建页目录项
109 .create_pde:
110 mov eax, PAGE_DIR_TABLE_POS
111 add eax, 0x1000 ;此时eax为第一个页表的位置
112 mov ebx, eax
113
114 ;下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存
115 ;页目录表的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问
116 or eax, PG_US_U | PG_RW_W | PG_P
117
118 ;在页目录表中的第1个目录项中写入第一个页表的地址(0x101000)和属性
119 mov [PAGE_DIR_TABLE_POS + 0x0], eax
120
121 mov [PAGE_DIR_TABLE_POS + 0xc00], eax
122
123 ;使最后一个目录项指向页目录表自己的地址
124 sub eax, 0x1000
125 mov [PAGE_DIR_TABLE_POS + 4092], eax
126
127 ;下面创建页表项(PTE)
128 mov ecx, 256 ;1M低端内存/每页大小4K=256
129 mov esi, 0
130 mov edx, PG_US_U | PG_RW_W | PG_P
131 .create_pte: ;创建page table entry
132 mov [ebx + esi*4], edx
133 add edx, 4096
134 inc esi
135 loop .create_pte
136
137 ;创建内核其他页表的PDE
138 mov eax, PAGE_DIR_TABLE_POS
139 add eax, 0x2000 ;此时eax为第二个页表的位置
140 or eax, PG_US_U | PG_RW_W | PG_P
141 mov ebx, PAGE_DIR_TABLE_POS
142 mov ecx, 254 ;范围为第769~1022的所有目录项数量
143 mov esi, 769
144 .create_kernel_pde:
145 mov [ebx + esi*4], eax
146 inc esi
147 add eax, 0x1000
148 loop .create_kernel_pde
149 ret
loader.S我还是将之前的代码附上了,便于理解,新增的是开启分页机制以及函数声明部分。
我们重点来看看setup_page函数,该函数的作用便是创建页目录表并初始化页内存。PAGE_DIR_TABLE_POS在boot.inc文件中定义为0x100000,也就是我们事先说的页目录表的存储地址。
首先将以PAGE_DIR_TABLE_POS为起始地址的4096个字节,也就是一页物理页大小的内存空间给清0,随后再进行初始化。代码中有很多注释,这里就不再赘述。
这里就不再赘述。还是和前面一样,通过nasm和dd命令将loader.S编译写入硬盘,运行boch得到如下画面。在boch的控制台输入info tab命令查看生成的页表。


左边是虚拟地址,右边是映射后的真实物理地址。对比我们前面设计的页表没有问题,说明我们的程序没有问题。
本回到此结束了,接下来我们要开始向内核进军,开始使用熟悉的C语言来编写程序了。欲知后事如何,请看下回分解。
电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。 准备工作: 1、U盘一个(尽量使用8G以上的U盘)。 2、一台正常联网可使用的电脑。 3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。 4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。 U盘启动盘制作步骤: 注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
我有一个使用SeleniumWebdriver和Nokogiri的Ruby应用程序。我想选择一个类,然后对于那个类对应的每个div,我想根据div的内容执行一个Action。例如,我正在解析以下页面:https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=puppies这是一个搜索结果页面,我正在寻找描述中包含“Adoption”一词的第一个结果。因此机器人应该寻找带有className:"result"的div,对于每个检查它的.descriptiondiv是否包含单词“adoption
我正在我的Rails项目中安装Grape以构建RESTfulAPI。现在一些端点的操作需要身份验证,而另一些则不需要身份验证。例如,我有users端点,看起来像这样:moduleBackendmoduleV1classUsers现在如您所见,除了password/forget之外的所有操作都需要用户登录/验证。创建一个新的端点也没有意义,比如passwords并且只是删除password/forget从逻辑上讲,这个端点应该与用户资源。问题是Grapebefore过滤器没有像except,only这样的选项,我可以在其中说对某些操作应用过滤器。您通常如何干净利落地处理这种情况?
在我做的一些网络开发中,我有多个操作开始,比如对外部API的GET请求,我希望它们同时开始,因为一个不依赖另一个的结果。我希望事情能够在后台运行。我找到了concurrent-rubylibrary这似乎运作良好。通过将其混合到您创建的类中,该类的方法具有在后台线程上运行的异步版本。这导致我编写如下代码,其中FirstAsyncWorker和SecondAsyncWorker是我编写的类,我在其中混合了Concurrent::Async模块,并编写了一个名为“work”的方法来发送HTTP请求:defindexop1_result=FirstAsyncWorker.new.async.
因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实
a=[3,4,7,8,3]b=[5,3,6,8,3]假设数组长度相同,是否有办法使用each或其他一些惯用方法从两个数组的每个元素中获取结果?不使用计数器?例如获取每个元素的乘积:[15,12,42,64,9](0..a.count-1).eachdo|i|太丑了...ruby1.9.3 最佳答案 使用Array.zip怎么样?:>>a=[3,4,7,8,3]=>[3,4,7,8,3]>>b=[5,3,6,8,3]=>[5,3,6,8,3]>>c=[]=>[]>>a.zip(b)do|i,j|c[[3,5],[4,3],[7,6],
我有一个非常简单的Controller来管理我的Rails应用程序中的静态页面:classPagesController我怎样才能让View模板返回它自己的名字,这样我就可以做这样的事情:#pricing.html.erb#-->"Pricing"感谢您的帮助。 最佳答案 4.3RoutingParametersTheparamshashwillalwayscontainthe:controllerand:actionkeys,butyoushouldusethemethodscontroller_nameandaction_nam
在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题: