文章目录
上一节:23、指令的格式及其操作尺寸
下一节:25、保护模式程序的动态加载和执行
01、MOV DS, AX和MOV DS, EAX
具体代码参考c11_mbr.asm
。视频里的程序和配套资料里的代码还不一样,但是基本功能类似。
计算GDT
的逻辑段地址
中使用除法div r/m32
,
其中指令mov ds, ax
在16位操作尺寸下机器码是8ED8
,在32位操作尺寸下机器码是668ED8
,但是Intel
的官方文档中对这种指令做了优化:
大致意思就是在16位和32位操作尺寸下,这条指令的机器码都是8ED8
。若加上前缀66
那么执行时需要多花一个执行周期。
所以Intel
官方手册建议使用mov ds, eax
来防止出现指令前缀66
,但是使用的NASM
编译器,不管操作尺寸是16位还是32位,编译之后都不会出现指令前缀66
。
编译之后查看lst
列表文件:
使用16位操作尺寸时:
使用32位操作尺寸时:
可以看出NASM
编译器确实将这种指令编译成了8ED8
。
此节程序就是设置栈、计算GDT
所在逻辑段地址
、安装描述符、初始化GDTR
、打开A20
、禁止中断、设置CR0
进入保护模式、跳转执行保护模式程序。
02、修改段寄存器时的保护
在上一节最后进入到保护模式之后,执行了远转移指令jmp
,此时给出的段选择子的T位
都是0,即要选择的描述符在全局描述符表GDT
中。同时也会做一些检查以确保选择描述符这一系列的过程正确。
1、检查第一步,如上图,处理器会检查访问的描述符的边界,若超过了边界条件,就会产生一个异常中断13
,同时段寄存器保持原来的值不变。
2、确定描述符的类型,比如描述符S=1、X=1
,表示此描述符代表一个代码段,只能加载
到段寄存器CS
。首先描述符类型(TYPE
)字段必须是有效的,000
就是无效的;接着检查描述符的类型是否与段寄存器匹配,参照下表进行检查:
3、检查描述符中的P位
是否为1,若为0表示虽然描述符已经定义,但是对应的段并不在物理内存中,此时处理器终止处理并引发11号中断
。一般来说需要定义一个中断处理程序接管11号中断
,将对应的描述符所指示的段调入内存,然后将P位
置1。这种类型的中断返回时并不是返回到原先的下一条指令,而是返回原先的指令,这样就能重新加载描述符。
若P位
是1,处理器将描述符加载到段描述符对应的描述符高速缓存器中,同时将A位
置1。
4、一旦上述验证通过,处理器就将段选择子
加载到段寄存器
的段选择器
中,
其中还有一些注意事项:
03、代码段执行时的保护
进入保护模式之后远转移指令jmp 0x0010:flush
会修改段寄存器CS
,会进行一系列检查工作:
- 首先选择子
0x0010
指定的描述符不能超过描述符表的边界; - 其次指定的描述符必须是代码段描述符,相关的信息必须是完整且合法的;
- 接着还要检查偏移量flush是否超出了当前段的界限。因为是刚进入保护模式,
CS
的描述符高速缓存器
的内容还是之前的值,段界限是0xFFFF
,
一旦通过了检查,就将选择子0x0010
带入CS
的段选择器
,并用描述符填充描述符高速缓存器,接着用偏移量flush
修改指令指针寄存器EIP
,处理器立即转入目标位置处执行。
因为这一条指令刷新了段寄存器CS
,导致处理器到一个全新的代码段执行。
这个描述符指定了D位是1
,因为这个描述符所指定的段在执行jmp
指令之前是在16位操作尺寸下运行的,之后是在32位操作尺寸下运行的。
在保护模式下一旦相应的描述符被加载到CS描述符高速缓存器,则处理器取指令和执行指令时就不再访问描述符表,而是直接使用CS描述符高速缓存器,从中取得线性基地址同指令指针寄存器EIP的值相加形成32位线性基地址,到内存中取得下一条指令。
不过在执行下一条指令执行之前,处理器必须检查这条指令的地址是否有效,以防止执行处理器不允许的指令。
04、用向上扩展的段做为栈段
选择01号
段:
01号
段描述符的具体状态如下:
基地址
:为0x00000000
S位
:为1
X位
:为0
,表示数据段E位
:为0
,表示向上扩展段界限
:为0xFFFFF
G位
:为1
,表示段的粒度为4K字节,段界限是以4K字节为单位
这与之前的段重叠了,我们知道代码段是不允许写入的,指的是不允许通过代码段的描述符来写入代码段,但是可以通过这个重叠的4G字节的段来写入代码段。
接下来设置栈段:
选择的是11号
描述符:
X位
:为0
,表示数据段B位
(由于这是数据段
):为1
,使用ESP
,(为0
使用SP
)E位
:为0
,表示向上扩展的基地址
:为0x00006C00
段界限
:为0x007FF
G位
:为0
,表示段界限按照字节划分
这个栈段是数据段,共2K字节。
栈的扩展方向
和推进方向
不同,段扩展方向不是数据的读写方向,而是用来定义偏移量的范围、处理器的界限检查等,如下:
因为这是数据段、所以是B位
,且B位
的值为1
,表示使用ESP
当作栈的指针,所以要设置ESP的初始值为段地大小(段界限0x7FF + 1 = 0x800
)。
05、向上扩展的段作为栈段时的保护
上一节中使用的栈段为2K字节:
且使用ESP
当做栈指针寄存器来进行隐式的栈操作指令,如:
这些指令操作时,会对栈进行压栈、出栈。
使用下列两条代码演示栈的检查:
在压入时选哟对偏移量进行检查:
第一条指令压栈后的状态,和检查过程:
若将ESP
初始值设置为0x802
,再进行压栈指令时就会将数据压入到栈的边界之外,就会导致处理器产生中断。
06、访问普通数据段时的保护
将上一节压入的数据弹出:
当前栈的布局:
首先需要检查出栈的数据是否超出栈的边界之外:
检查通过之后:
1、使用段SS
的基地址0x00006C00
加上 当前ESP
中的偏移值0x07F8
得到有效地址0x000073F8
,从这个地址处取出4个字节数据。
2、之后使用默认的数据段DS
段地址0x00000000
加上指令中给出的偏移量0x0B800
得到有效地址为0x000B8000
,将栈中弹出的数据写入此处。
在写入之前,处理器要检查是否会超出段界限之外:
写入之后数据段DS
的状态:
3、出栈之后,处理器将ESP的值加4
变为0x07FC
07、内存线性地址的回绕特性
段的逻辑地址为0x0000000
,加上右边的偏移量,得到逻辑地址。
还可以使用下列方式表示偏移量:
08、用向下扩展的段做为栈段
之前使用向上扩展的段作为栈段,大小为2K字节。
描述符:
- S位:为1,表示为寄存器的段描述符
- X位:为0表示数据段描述符,为1表示代码段描述符
- E位:为0表示向上扩展,为1表示向下扩展
向下扩展的数据段实际允许使用的偏移量范围 = 实际允许使用的段界限 + 1开始的,段内偏移量的最大值为0xFFFF
或0xFFFFFFFF
。
D/B位
:对于代码段是D位
、数据段是B位
,B位为0
表示段内偏移量的最大值是0xFFFF
,B位
为1表示段内偏移量的最大值是0xFFFFFFFF
。
还有B位
若为0
使用SP
,为1
使用ESP
。
对于向下扩展的段:
程序中使用第二个栈段:
程序中使用第二个栈段描述符:
S位
:为1
表示寄存器的段描述符X位
:为0
表示数据段基地址
:为0x00007C00
E位
:为1
表示向下扩展W位
:为1
表示可读可写段界限
:为0xFFFFE
G位
:为1
表示粒度为4K字节,即段界限以4K字节为单位B位
:为1
表示偏移量理论最大值是0xFFFFFFFF
,为1也表示使用ESP
这个新的段,状态如下:
ESP
的值要设置为站的最大界线值 + 1:0xFFFFFFFF + 1 = 0x00000000
。
09、向下扩展的段作为栈段时的保护
上一节中设置的栈:
使用下列指令执行压栈操作:
在执行压栈指令时,需要的压入的数据进行检查,以确定不会超出端的界限:
具体为处理器先将ESP
减 4
,即0 - 4 = 0xFFFFFFC
,则0xFFFFFFC
> 实际使用的段界限0xFFFFEFFF
,符合上述条件执行压栈操作。
接下来处理器使用段的基地址0x00007C00
加上操作数的有效地址0xFFFFFFFC
= 0x00007BFC
,从这里写入数据。
压栈之后执行出栈操作;
具体操作过程参考第05
节即可。
10、通过别名来实现段的共用和共享
在保护模式下不能使用CS
来修改内存,但是可以设置一个代码段的别名描述符,将其X位
设置为0
表示数据段,这样就可通过ES
来访问字符串所在内存位置,以达到修改内存的操作。
11、冒泡排序的基本原理
排序5、9、3、7
:比较次数为长度4减1为3次
。
第一遍比较:选出最大数字9
5、9、3、7
5、3、9、7
5、3、7、9
第二遍比较:选出最大数字7
3、5、7
3、5、7
第三遍比较:选出最大数字5
3、5
最后排序完成为:3、5、7、9
12、32位操作尺寸下的LOOP指令
具体代码为c12_mbr.asm
。
若CS
高速缓存寄存器中的D位为0
表示使用16位操作尺寸,那么loop
指令使用cx
计数;D位为1
表示使用32位操作尺寸,那么loop
指令使用ecx
计数,
冒泡排序外循环:@@1
处
冒泡排序内循环:@@2
处
其中外循环比较次数:可以看出内循环比较次数和外循环ECX
相同。
冒泡内循环比较过程:
13、数据交换指令XCHG
具体代码为c12_mbr.asm
。
XCHG
指令:操作数不能同时为内存地址
上一节:23、指令的格式及其操作尺寸
下一节:25、保护模式程序的动态加载和执行