dustland

dustball in dustland

x86汇编语言 chapter 11 保护模式

chapter 11 保护模式

全局描述符表

32位保护模式下任何段使用之前都需要注册登记,否则不让用,

注册时还需要说明该段的访问权限,如果一个只能读写的段非要在上面执行代码会被制止

如果访问范围超过了段的界限也会除法处理器产生内部异常中断

注册登记段信息的地方就是描述符表,描述符表有全局的GDT也有局部的LDT

全局描述符表是给整个系统服务的,处理器从实模式进入保护模式之前必须设置好全局描述符表,即GDT是在实模式下建立的,那么其内存地址应该不超过8086的寻址范围1M,(在进入保护模式之后搬到别的地方另说)

处理器怎么直到GDT放到内存上哪里了呢?全局描述符表寄存器GDTR就是干这个事的--它专门记录全局描述符表在内存中的位置

GDTR有48位,高32位记录的是全局描述符表的基地址,低16位记录的是该表的界限,因此该表可以在在32位可寻址的4G内存的任何地方,长度最长是64KB.

又全局描述符表的表项一条是8字节,因此该表最大可以有64K/8=8K条记录

image-20220829164636897
image-20220829164840344

段选择子

段描述符

用段寄存器中存放的选择子中的索引查段描述符表得到段描述符,段描述符相当于一个保存段信息的结构体

段描述符就是描述符表GDT或者LDT等的表项,每个段描述符长8字节

image-20220829165349719

这个段描述符长的很不顺溜,段基地址被分成了三块,段界限被分成两块

这样设计是为了和废物16位保护模式兼容

基址和界限

段基地址共32位,段界限共20位,即一个段的基地址可以是4G地址空间中的任何地方,段大小最大是1M(或4G,取决于粒度G的规定)

粒度G

Granularity,粒度,用于解释段界限的含义

段界限占用了16位,如果以1B为单位,则一个段最大是\(2^{20}\times 1B=1MB\)大小

然而4G的地址空间应该允许以G为量级的段

当段界限的单位是4KB时则一个段最大是\(2^{20}\times 4KB=4G\)

为啥要以4KB为单位?因为分页时一页的大小就是4KB,这样规定粒度方便给一个段分配页数

G=0表示段界限的单位是1B

G=1表示段界限的单位是4KB

段描述符类型S

S=0表示系统段

S=1表示代码段或者数据段

描述符特权级DPL

Descpirtor Privilege Level

指定要访问该段需要最低的权限

即0环还是3环,规定段级别,

0环为最高级,只能由系统访问

3环为最低级,可以由系统或者用户程序访问.

段存在位P

Segment Present

P=0表示段不存在于物理内存中,即建立了描述符但是尚未建立对应物理页,或者刚才该段在内存中存在但是现在被交换到了磁盘中,也需要把P置0

P=1表示该段已经在物理内存中了

该位用于触发缺页中断,属于虚存调度策略

默认操作数大小D/B

Default Operation Size

用于兼容16位保护模式

对于代码段,该位是D位

D=0表示指令中的偏移地址和操作数都是16位的,比如使用ax,ip等16位的寄存器,不使用eax,eip等32位寄存器,即使eax的高16位有东西也忽略

D=1则是32位的

对于数据段,该位是B位

B=0表示16位的,B=1表示32位的

B=0使用16位栈顶指针sp,不使用esp,栈边界也是16位的

描述符子类型TYPE

TYPE占用了4位,分别是X(执行),E/C(拓展方向/特权依从),W(写)A位

image-20220829172645403

A位不管是代码段还是数据段,都表示是否已访问(Access),属于虚存调度的范畴

对数据段

E指定的拓展方向,该段是往地址增大的方向生长,比如堆;还是往地址减小的方向生长,比如栈

W=0表示只读,W=1表示读写,不管怎么找,必须有读的权限

X=0表示不可执行,X=1表示可执行,可以猜测NX保护就是修改的该位

对代码段

C表示是否特权级依从,这里的特权级就是DPL指定的段描述符特权级

C=0表示可以被同级段调用

C=1表示可以被低级段调用

R表示是否可读,

R=0不可读

R=1可读

不管怎么着,代码段一定是不可写,可执行的

这里的可读不可读是对程序的限制,不是对处理器的限制,处理器从代码段取代码是不受限制的,但是程序如果尝试使用[cs:offset]从代码段取东西看看,是不被允许的

软件可用位AVL

操作系统使用,处理器不管这一位.算是预留的一位

64位代码段标记L

L=1表示64位

L=0表示32位

32位下该位置0

走向保护模式

例子中将栈安排在0x7C00开始往低地址方向生长

主引导程序512个字节占据从0x7C00开始到0x7E00

从0x7E00开始的64K到0x17DFF是GDT

image-20220829191817146

计算GDT所在的逻辑段地址

1
2
3
4
5
6
7
8
9
10
;计算GDT所在的逻辑段地址 
mov ax,[cs:gdt_base+0x7c00] ;低16位
mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位
mov bx,16
div bx
mov ds,ax ;令DS指向该段以进行操作
mov bx,dx ;段内起始偏移地址
....
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址

0x7c00是本程序加载到内存中的位置

cs:gdt_base是该标号的汇编地址

两者加起来才得到该标号的物理地址也就是gdt_base的地址

把这个地址开始的四个字节0x00007e00放到dx:ax里,然后除以16,商放到ds里作为段地址,余数放到bx里作为段内起始偏移地址

此时还处在实模式,因此段地址除以16再交给段寄存器

从ds:bx开始就是全局段描述符表了

创建段描述符

Intel处理器要求0号段描述符为空,有意义的段描述符从1号开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00

;创建#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800

;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
mov dword [bx+0x10],0x8000ffff
mov dword [bx+0x14],0x0040920b

;创建#3描述符,保护模式下的堆栈段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600

这里1号段描述符的意义是:

段基址0x00007c00,恰好是主引导记录加载到内存中的地址

段界限0x001ff,段长度为512字节

G=0,粒度为字节,

D=1,32位段

L=0,非64位段

AVL=0

P=1,目前位于内存中

DPL=00,0环

S=1,代码段

TYPE(XCRA)=1000,只能执行,向上拓展

初始化段描述符表寄存器

1
2
3
4
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一)

lgdt [cs: gdt_size+0x7c00]

算上没有意义的0号描述符,一共有四个描述符,共32字节,因此GDT表的界限应该是31,放到gdt_size中

lgdt指令用于加载GDT表地址到GDTR寄存器,其操作数是一个48位数,也就是内存中6个字节,高32位是GDT地址,低16位是GDT界限

这里使用lgdt [cs: gdt_size+0x7c00]意思是从gdt_size标号开始的48位,低16位作为GDT界限,高32位作为GDT地址,放到GDTR中

而gdt_size开始的内存是这样定义的:

1
2
gdt_size         dw 0		;低地址,一个字,16
gdt_base dd 0x00007e00 ;高地址,一共双字,32

由于先前用mov word [cs: gdt_size+0x7c00],31已经设置好了gdt_size

这里lgdt准确地将GDT的地址和界限放到了GDTR中

A20与地址回绕

8086只有20根地址总线A0-A19,不存在A20这根线.

在8086时,地址最大值是0xFFFFF,再加1就高位截断了成了0x00000,这就是地址回绕.书上说当时很多程序员利用这个"特性"编程,并且好像还那个以此为自豪

80286时地址线就有24根儿了,0xFFFFF+1=0x100000,因为位数足够多,不会高位截断,也就不再回绕了,这样原来利用地址回绕写的程序全都寄了.

为了保持兼容性,保持一下这些程序员的自尊心,IBM在A20上设置了一个开关,兼容8086时就不用A20,让他一直置0,这就有了0x0FFFFF+1=0x000000,又绕起来了.IBM把A20和键盘控制器上一个开关按位与了再接到内存条子上,这个开关的端口号0x60.

向0x60端口写入数据,第一位置1则该键向与门输出1,此时A20生效

向0x60端口写入数据,第一位置0则该键向与门输出0,此时A20失效

image-20220829203454342

80486以后处理器有了A20M#引脚,低电平时A20失效

image-20220829203538236

向0x92端口的第二位(位1)置1就打开了A20,A20有效.置0则A20失效.

开机时自动置有效

0x60和0x92关于A20的控制是或,即只要有一个开关打开,A20就有效

而要从实模式转换为32位保护模式,显然需要打开A20

1
2
3
in al,0x92                         ;南桥芯片内的端口 
or al,0000_0010B
out 0x92,al ;打开A20

in就是从0x92读取一个字节的数据放到al寄存器

然后通过按位或将al的第二位(位1)置高,其他位不变

然后out将al输出到0x92一个字节

这就设置好了0x92端口处的快速A20和初始化寄存器.A20就打开了

关闭中断

进入保护模式后,BIOS提供的实模式下的中断功能不能再使用,而保护模式的中断环境尚未设置,因此进入保护模式前需要先关闭中断

1
cli

CR0与保护模式

控制CPU运行模式的开关在CR0寄存器

CR0的最低位(位0)如果是1则CPU进入保护模式,置0则为实模式

至于CR0其他位干啥的现在不关心

1
2
3
mov eax,cr0                    ;cr0放到eax
or eax,1 ;低位置1
mov cr0,eax ;设置PE位

此后CPU就工作在保护模式了

32位机器上的段寄存器

image-20220829210802621

高16位是段选择子,对外可见,并且兼容8086的段寄存器用法

描述符高速缓存器不可见,存放段基地址,段界限,段属性

为啥叫缓存器呢?

32位实模式段寄存器用法

8086实模式下,段寄存器中直接放段基址,段寄存器就是16位,没有描述符高速缓存器这种东西,寻址的时候就段寄存器×16+偏移量

而32位机器的实模式,前16位和8086的段寄存器作用相同,但是有高速缓存器这种东西

它缓存了个啥呢?寻址的时候不是要段寄存器×16吗,高速缓存器就缓存了这个值(聊胜于无吧)

给段寄存器赋值的时候就把该值✖16然后放到高速缓存器中了

对外表现仍然像8086的20位实模式,只不过由于高速缓存器的存在,速度更快了

32位保护模式段寄存器的用法

32位保护模式下,段寄存器CS,DS等等仍然是16位的,显然让他们继续保存段基址已经放不下了,他们确实也不再直接保存段地址,而是保存的段选择子,

段选择子是段描述符表的下标,

即用段选择子去查相应的段描述符表,得到的表项是段描述符,

段描述符中包含了段基址,界限,段类型等等各种信息

1
2
3
实模式:查段寄存器立刻获得段基址
32位保护模式:查段寄存器中的段选择子获得段描述符表下标,查段描述符表获得段描述符
段描述符包含段基址信息
image-20220829170144452

高13位就是段描述符表中的下标,13位可以寻址8K条记录,这和段描述符表最大记录数量是一致的

再低一位是全局/局部 段描述表标志,如果是0则该选择子中的索引是全局描述符表的下标

如果是1则该选择子中的索引是局部描述符表的下标

最低的两位是请求特权级RPL,表示给出该选择子的程序的特权级

这里要区分段描述符中的DPL和段选择子的RPL

DPL表示的是该段的特权级

RPL表示需要访问该段的程序的特权级

高速缓存器的作用是啥呢?在段描述符表中也有段的基址,界限,属性,为啥又要在描述符高速缓存器中再写一遍?这就是"缓存"的作用.如果没有高速缓存寄存器,那么每次使用这个段,都需要用段选择字查段描述符表获得段描述符指定的基址和界限,这就涉及到内存访问了.如果第一次放问该段时查表获得了段基址,把他存到高速缓存器中,那么下一次使用这个段的时候,就不需要访问内存了.显然访问寄存器速度比访问内存快

保护模式下的内存访问

数据段当初是这样创建的

1
2
3
;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 
mov dword [bx+0x10],0x8000ffff
mov dword [bx+0x14],0x0040920b

段基址指向0xb8000,即显存区域

mbr程序中,将ds寄存器置为数据段的选择子,数据段描述符在全局段描述符表的第3项,下标为2,权限为00,因此将0x10放到ds中作为段选择子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flush:
mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,cx

;以下在屏幕上显示"Protect mode OK."
mov byte [0x00],'P' ;这里[0x00]默认使用ds指向段
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'

加载描述符高速缓存器

mov ds,cx这条改变段寄存器ds的指令执行之后,处理器会自动查GDT表获取段基址,界限,属性,填到高速缓存器中

这个过程用图表示为

image-20220829213426419

用段寄存器中的索引值乘以8是因为,GDT表的表项8字节,加上GDTR中存放的GDT表基地址,就得到了相应表项的起始地址,从该段描述符中获取相关信息填到高速缓存器中

地址翻译

由于访问内存前首先要设置段寄存器,只要是使段寄存器发生变化的指令,比如mov,jmp far,call far等,都会导致处理器自动加载高速缓存器

那么当实际需要访问内存的时候,高速缓存器已经加载好了,基地址是0xb8000,

这时高速缓存器中的基址等信息和用选择子查GDT表获取到的基址等信息是相同的,因此不需要再查段地址了

image-20220829213823071

mov byte [0x00],'P'这条指令,默认使用ds指向的数据段,偏移量0x00,

寻址的时候只需要从ds段寄存器的描述符高速缓存器中,

把缓存好的数据段界限拿出来,和偏移量比一下,看看该偏移量是否越界了,如果没有则

把缓存好的数据段基址拿出来,加上该偏移量,得到32位线性地址0xb8000

地址总线上0x000b8000信号置高

然后把'P'的ASCII码放到数据总线上,置高

然后CPU发出内存写指令,'P'就写到内存的0x000b8000位置了,这个位置恰好又是显存映射区,因此直接输出到屏幕了

这里地址翻译的结果是"线性地址",不是物理地址,这是因为,如果使用了分页机制,那么该线性地址有可能不等于物理地址,其所在虚拟页号不一定等于物理页号

如果没有使用分页机制,那么可以说线性地址就是物理地址

取指过程也类似

image-20220829214456572

清空流水线

在进入保护模式前,段寄存器以及高速缓存器已经有东西了,进入保护模式需要更新这些值.并且很多实模式的指令已经在流水线上了,进入保护模式后不再适用,需要清空流水线

使用远jmp或者远call,既可以更新段寄存器,又可以把流水线扬了

因此在设置PE位之后有一个jmp dword跳转

jmp dword 32位远跳转指令

1
2
3
4
5
6
7
8
9
10
11
12
     mov eax,cr0                    ;cr0放到eax
or eax,1 ;低位置1
mov cr0,eax ;设置PE位

;以下进入保护模式... ...
jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
;清流水线并串行化处理器
[bits 32] ;伪指令,此后用32位编译

flush:
mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,cx

这里jmp dword 0x0008:flush

意思是跳转到一个32位地址,段选择子是0x0008,(即GDT索引为0x1,G=0,RPL=00)偏移量为flush

dword修饰意思是使用32位的偏移量,编译成的机器码带有前缀0x66,表示处理器会按32位的方式执行该指令

由于当前已经处于16位保护模式,又dword表明使用32位方式执行,因此cs的段选择子会被置为0x8,高速缓存器也会查GDT后填入0x7c00基址,0x1ff界限

flush就交给EIP寄存器

因为代码段可能有转移,刚才顺序执行的流水线无效了,全都扬了

然后[bits 32]是nasm伪指令,意思是后面的代码编译成32位模式

但是保护模式下不允许使用mov指令修改CS寄存器内容,就算用ax寄存器中转也白搭.

只是对于CS寄存器有这个限制,其他段寄存器没有限制

保护模式的栈

堆栈段描述符

GDT中堆栈段描述符长这样

1
2
3
;创建#3描述符,保护模式下的堆栈段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600

线性基地址0

段界限0x7A00,最大7A00字节

粒度G=0字节

D=1,32位段,默认push压栈4个字节,使用esp(如果是D=0,16位,则默认push压栈字,使用sp)

S=1,数据段

P=1,在内存中

DPL=0,0环

TYPE=0010 可读写,向下生长

初始化堆栈

1
2
3
mov cx,00000000000_11_000B         ;加载堆栈段选择子
mov ss,cx
mov esp,0x7c00

选择子意思是下标0x11=3,查全局段描述符表,0环权限

将该段选择子放到ss堆栈段寄存器,将引起处理器自动查GDT表获取段基址放到段描述符高速缓存器中

然后将esp置为0x7c00表示栈顶指针位置,

对于esp,有一个要求,esp>粒度×界限,也就是说esp最低要保证栈空间满足粒度呈×界限这么多,但是高不封顶

啥意思呢?你不是esp要比粒度×界限大吗,我大一个字节也是大,大10个字节也是大,esp顶到天上也是大

image-20220829223100310

esp的变化方向将会是0x7c00->0

使用堆栈

1
2
3
4
5
6
7
8
9
10
     mov ebp,esp                        ;保存堆栈指针 
push byte '.' ;压入立即数(字节)

sub ebp,4
cmp ebp,esp ;判断压入立即数时,ESP是否减4
jnz ghalt
pop eax
mov [0x1e],al ;显示句点
ghalt:
hlt ;已经禁止中断,将不会被唤醒

ebp获得esp拷贝

push byte指令导致'.'压栈,但是实际压入栈中的是一个双字,esp会减4,在esp+1放上'.',在esp+2,esp+3,esp+4都放0

为了证实这一点,ebp直接-4,如果刚才的理论正确,则ebp此时应该等于esp,那么cmp指令将会把ZF=1置起来.那么jnz跳转不实现,那么将栈上刚压入的一个四字推给eax寄存器,esp+4恢复原样,然后al字节放到显存[0x1e]上打印句点到屏幕

也就是说只要是运行起来最后有句点,说明理论正确

image-20220829224646277

运行结果确实有句点,证明push byte '.'导致栈顶下降了4字节

调试

可能会预见的问题

每次虚拟机运行关闭之后,都会在虚拟硬盘目录下面产生一共.lock文件

image-20220829232306055

只要是有这个东西下一次开机虚拟机准起不来

扬了就行了

观察处理器上电后的段寄存器状态

使用bochs调试运行nobody.vhd,bochs会自动在第一条指令指向前停下

此时用r观察所有寄存器状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bochs:1> r
rax: 00000000_00000000
rbx: 00000000_00000000
rcx: 00000000_00000000
rdx: 00000000_00000000
rsp: 00000000_00000000
rbp: 00000000_00000000
rsi: 00000000_00000000
rdi: 00000000_00000000
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_0000fff0
eflags 0x00000002: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af pf cf

除了程序计数器rip,其他寄存器全是0

使用sreg观察所有段寄存器状态

bochs可以观察高速缓存器的内容

dh,dl是段描述符的内容,显然此时还没有建立GDT,dh和dl的值是bochs根据高速缓存器的值造的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bochs:2> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
Data segment, base=0xffff0000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x0000000000000000, limit=0xffff
idtr:base=0x0000000000000000, limit=0xffff

只有cs段寄存器的基地址是0xf0000,其他都是0

lgdt之后全局gdtr寄存器的变化

lgdt以内存操作数的低16位为界限,高32位为基址,加载gdt表信息到gdtr寄存器

怎么观察这个事呢?

首先需要找到lgdt的地址,可以在0x7c00处下断点,按c执行到此,然后u/20反汇编20条指令,观察这些指令的地址

image-20220829230722202

这就找到了lgdt的地址,然后再0x7c5f上下断点,按c执行到此

执行之前sreg打印一下gdtr的状态

1
gdtr:base=0x00000000000f9ad7, limit=0x30

s单步执行之后再打印一下gdtr的状态

1
gdtr:base=0x0000000000007e00, limit=0x1f

使用xp/8 0x7e00观察GDT表

1
2
3
4
<bochs:13> 0x/8 0x7e00
[bochs]:
0x0000000000007e00 <bogus+ 0>: 0x00000000 0x00000000 0x7c0001ff 0x00409800
0x0000000000007e10 <bogus+ 16>: 0x8000ffff 0x0040920b 0x00007a00 0x00409600

0x7e00处是全空的0下标段描述符,后面的段描述符是有实际意义的

置PE位后段寄存器的变化

通过设置CR0的PE位,处理器进入保护模式,段寄存器仍然保存了实模式下的内容,除非有修改段寄存器的指令

怎么观察这个事呢?

首先需要找到即将进入保护模式的指令

可以使用u/balabala 反汇编一坨指令找他

1
0000000000007c73: (                    ): mov cr0, eax              ; 0f22c0

在0x7c73下断点然后c执行到此

image-20220829231354276

执行前后用sreg观察段寄存器是没有任何变化的

后面jmp dword远跳转就会改变段寄存器了,首先反汇编找到该远跳转的地址

1
0000000000007c76: (                    ): jmpf 0x0008:0000007e      ; 66ea7e0000000800

在0x7c76下断点然后c执行到此

执行前先sreg打印一下CS的状态

1
2
cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed

s单步执行后再sreg观察cs的状态

1
2
cs:0x0008, dh=0x00409900, dl=0x7c0001ff, valid=1
Code segment, base=0x00007c00, limit=0x000001ff, Execute-Only, Non-Conforming, Accessed, 32-bit

此时dh,dl都指向了GDT中的信息,cs存放的是选择子

高速缓存器中的base和limit业已设置好了

观察控制寄存器CR0的变化

重新调试运行,找到设置PE位的指令

1
0000000000007c73: (                    ): mov cr0, eax              ; 0f22c0

在0x7c73下断点然后c运行到此

执行前creg打印一下CR0的状态

1
2
<bochs:6> creg
CR0=0x60000010: pg CD NW ac wp ne ET ts em mp pe

此时的pe=0表明处理器工作在实模式

然后s单步执行之后creg观察CR0

1
2
3
4
5
<bochs:7> s
Next at t=17179024
(0) [0x000000007c76] 0000:0000000000007c76 (unk. ctxt): jmpf 0x0008:0000007e ; 66ea7e0000000800
<bochs:8> creg
CR0=0x60000011: pg CD NW ac wp ne ET ts em mp PE

果然PE=1了,表明处理器工作在保护状态