目录
一、前景回顾
二、A20地址线
三、全局描述符表
四、CR0寄存器的PE位
五、迈入保护模式
六、测试
上回我们说到,保护模式下有着三大特点:地址映射、特权级和分时机制。本来接下来是要向这三点一一发起进攻,不过我们首先需要先迈入保护模式中,不然在实模式下讲解保护模式显得不伦不类。怎么进入保护模式呢?其实也很简单,就三个步骤:
1、打开A20地址线
2、加载全局描述符表GDT
3、将CR0寄存器的pe位置1
我们知道在8086CPU中,只有20位地址线,即A0~A19。20位地址总线表示的内存范围是1MB,即0x0~0XFFFFF,若内存超过了1MB,是需要第21条地址线也就是A20支持的,所以说,如果地址进位到1MB以上,如0x100000,由于没有A20地址线的支持,相当于丢掉高位的1,导致地址变成了0x00000。对于一开始的8086来说,是没有A20地址线的说法的。随着后来的80286CPU的诞生,24位地址总线出现了,从而能够访问的内存范围达到了16MB,为了兼容之前的16位CPU,于是有了A20地址线。80286运行在实模式下时,A20地址线是关闭的,当访问的地址超过1MB,便会自动回卷到0x00000开始;当运行在保护模式下后,A20地址线又被打开,访问超过1MB的地址,系统将会直接访问这块物理内存。
总之我们现在想要进入保护模式,就需要开启A20地址线,方法也很简单,只需要下面3行代码:
1 in al, 0x92
2 or al, 0000_0010B
3 out 0x92, al
在保护模式下,内存段(如数据段、代码段等)不再是简单地用段寄存器加载一下段基址就能使用,因为前面我们说了,保护模式下增加了“保护”的作用,所以对于每一个内存段来说,增加了一些信息,需要提前把段定义好才能使用。就像家庭成员需要上户口一样,在户口簿上登记好了才算合法。
这些信息比较繁杂,光靠寄存器来存储是不行的,哪怕现在有了32位寄存器。所以我们将关于内存段的这些描述信息给存储在内存中。
我们需要哪些属性来描述内存段呢?
首先,要解决实模式下存在的问题。实模式下的用户程序是可以破坏存储代码的内存区域,所以需要添加一个内存段类型属性来阻止这种行为;实模式下的用户程序和操作系统是同一级别的,所以需要添加一个特权级属性来区分用户程序和操作系统,这个特权级属性在后面的特权级章节会提到。其次就是一些访问内存段的必要属性条件,内存段是一片内存区域,就需要提供这段内存区域的段基址,是一个区域,就会有范围,还需要对段的大小进行限制,避免越界访问。上面的这几个属性就是比较重要的,还有一些其他约束属性会在后面的章节中提到。
总之,最后这些用来描述内存段的属性就被放到了一个称作段描述符的结构中,该结构专门用来描述一个内存段,大小为8字节。如下图所示:

低32位中,前16位用来存储段的段界限的前0~15位,后16位为段基址的前0~15位。
再看高32位,0~7位是段基址的16~23,24~31是段基址的24~31,这样就得到了段基址的32位地址。
一个段描述符用来定义一个内存段,那么多个内存段就需要多个段描述符,我们将这些段描述符存放在一个叫做全局描述符表的地方,全局描述符表就相当于是一个段描述符数组。全局描述符表的地址被存放在一个叫做GDTR的寄存器中,这样CPU就能通过这个寄存器来得到全局描述符表的地址,进而获取到各个段描述符的信息。

GDTR寄存器中的前0~15位存放的是GDT界限,其实就是当前GDT所占用的字节数减去1。我们知道一个段描述符占用内存8字节,GDT界限最大能表示范围为2^16=64KB大小,也就是说GDT最多能存放64KB/8B=8192个段描述符。
现在段描述符和全局描述符表都有了,我们该如何使用呢?
还是和实模式下作对比,我们知道实模式下段寄存器存放的是段基址,但是保护模式下,我们已经有了段描述符和全局描述符表后,段寄存器中就不再存放段基址,而是存放段选择子,结构如下:

第3~15位存放的是索引值,通过该索引值在GDT中索引相对应的段描述符,从而得到段基址。我们可以看到3~15位有13位,刚好可以表示到2^13=8192,这与我们GDT的范围刚好对应上。
RPL字段表示的是请求者的当前特权级,会在后面的特权级章节中提到。TI用来指示选择子是在GDT还是LDT中索引段描述符。LDT现在已经不再使用了,这里就不再拓展。
总结一下现在段描述符和内存段的关系:

我们现在来看如何根据选择子和偏移地址来得到访存地址。
假如现在选择子是0x08,将其加载到ds寄存器后,访问ds:0x9这样的内存,其过程是:CPU会检查选择子0x08的低2位,00,表示当前的RPL,第3位为0,表示在GDT中索引段描述符,于是用高13位0x01在GDT中索引段描述符,得到段描述符中的段基址0x1234,将段基址0x1234加上段内偏移地址0x09相加,得到0x1234+0x9=0x123d,用0x123d作为访存地址。
CR0是CPU的控制寄存器之一,PE位,即Protection Enable,此位用于启动保护模式,是保护模式的开关,打开此位后,CPU就真正进入了保护模式。

置PE位为1也简单,输入如下代码:
1 mov eax, cr0
2 or eax, 0x00000001
3 mov cr0, eax
我们已经将迈入保护模式的三个步骤讲的比较清楚了,现在在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 jmp $
loader.S继续完善boot.inc文件:
1 ;--------------------loader和kernel ---------------
2 LOADER_BASE_ADDR equ 0x900
3 LOADER_START_SECTOR equ 0x2
4 ;-------------------gdt描述符属性------------------
5 ;使用平坦模型,所以需要将段大小设置为4GB
6 DESC_G_4K equ 100000000000000000000000b ;表示段大小为4G
7 DESC_D_32 equ 10000000000000000000000b ;表示操作数与有效地址均为32位
8 DESC_L equ 0000000000000000000000b ;表示32位代码段
9 DESC_AVL equ 000000000000000000000b ;忽略
10 DESC_LIMIT_CODE2 equ 11110000000000000000b ;代码段的段界限的第2部分
11 DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;相同的值 数据段与代码段段界限相同
12 DESC_LIMIT_VIDEO2 equ 00000000000000000000b ;第16-19位 显存区描述符VIDEO2 书上后面的0少打了一位 这里的全是0为高位 低位即可表示段基址
13 DESC_P equ 1000000000000000b ;p判断段是否在内存中,1表示在内存中
14 DESC_DPL_0 equ 000000000000000b
15 DESC_DPL_1 equ 010000000000000b
16 DESC_DPL_2 equ 100000000000000b
17 DESC_DPL_3 equ 110000000000000b
18 DESC_S_CODE equ 1000000000000b ;S等于1表示非系统段,0表示系统段
19 DESC_S_DATA equ DESC_S_CODE
20 DESC_S_sys equ 0000000000000b
21 DESC_TYPE_CODE equ 100000000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清0
22 DESC_TYPE_DATA equ 001000000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上拓展,可写,已访问位a清0
23
24 DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00 ;代码段的高四个字节内容
25 DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00 ;数据段的高四个字节内容
26
27 DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B
28
29
30 ;------------选择子属性------------
31 RPL0 equ 00b
32 RPL1 equ 01b
33 RPL2 equ 10b
34 RPL3 equ 11b
35 TI_GDT equ 000b
36 TI_LDT equ 100b
boot.inc回到loader.S中。
第3行的LOADER_STACK_TOP equ LOADER_BASE_ADDR,LOADER_STACK_TOP表示的是保护模式下的栈,我们进入到保护模式后,也需要为程序指定栈顶指针,这里我们就将0x900赋给esp作为栈顶指针。
第7~14行,便是GDT的构建。这里我们实现定义了4个段描述符,第一个段描述符为空,这是GDT表的规定。从第二个开始依次是代码段描述符、数据段和栈段描述符、显存段描述符 。
第16~17行通过地址差来获得GDT的大小,进而用GDT大小减去1来得到GDT界限,这是为了加载GDT做准备。
第18行预留60个段描述符的位置,这是为了以后拓展做准备。
第20~22行是在构建代码段、数据段和栈段、显存段的选择子。
第25行构建了一个gdt_ptr指针,该指针指向的地址包含有6个字节的数据。
第29~41行便是按照前面提到的三个步骤开始从实模式进入保护模式。
第43行,使用jmp命令来刷新指令流水线,因为CPU并不知道自己即将会进入保护模式下运行,所以CPU会把后面的代码依旧按照实模式下16位指令格式来译码,而这与我们期望的不符。
第45行的[bits 32]是编译器的伪指令,目的是告诉编译器,将后面的代码都按照32位来译码。
第46~53行便是对一系列寄存器的初始化。
第55行,只是为了测试用,当代码执行到此处时,会在屏幕的第二行开始显‘P’字符。
第56行,让CPU悬停在此。
通过nasm和dd命令将mbr.S和loader.S编译写入到硬盘中,随后启动bochs,可以看到生成了预期的p字符。此外,还可以在boch的命令行窗口输入info gdt命令查看GDT表。可以看到GDT表中有四项,与我们事先的设计一样。


这一回就算结束了,内容还是比较多的。预知后事如何,请看下回分解。
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
电脑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.