存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18
本文是原书第12章的学习笔记。
说句题外话,这篇博文是补写的,因为让我误删了,可恶的是CSDN的回收站里找不到! 好吧,那就再写一遍,我有坚强的意志。司马迁曰:“文王拘而演《周易》;仲尼厄而作《春秋》;屈原放逐,乃赋《离骚》;左丘失明,厥有《国语》;孙子膑脚,《兵法》修列;不韦迁蜀,世传《吕览》……”好了,不煽情了,进入正题。
第12章的代码如下。
1 ;代码清单12-1
2 ;文件名:c12_mbr.asm
3 ;文件说明:硬盘主引导扇区代码
4 ;创建日期:2011-10-27 22:52
5
6 ;设置堆栈段和栈指针
7 mov eax,cs
8 mov ss,eax
9 mov sp,0x7c00
10
11 ;计算GDT所在的逻辑段地址
12 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位线性基地址
13 xor edx,edx
14 mov ebx,16
15 div ebx ;分解成16位逻辑地址
16
17 mov ds,eax ;令DS指向该段以进行操作
18 mov ebx,edx ;段内起始偏移地址
19
20 ;创建0#描述符,它是空描述符,这是处理器的要求
21 mov dword [ebx+0x00],0x00000000
22 mov dword [ebx+0x04],0x00000000
23
24 ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
25 mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xfffff
26 mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符
27
28 ;创建保护模式下初始代码段描述符
29 mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,512字节
30 mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符
31
32 ;创建以上代码段的别名描述符
33 mov dword [ebx+0x18],0x7c0001ff ;基地址为0x00007c00,512字节
34 mov dword [ebx+0x1c],0x00409200 ;粒度为1个字节,数据段描述符
35
36 mov dword [ebx+0x20],0x7c00fffe
37 mov dword [ebx+0x24],0x00cf9600
38
39 ;初始化描述符表寄存器GDTR
40 mov word [cs: pgdt+0x7c00],39 ;描述符表的界限
41
42 lgdt [cs: pgdt+0x7c00]
43
44 in al,0x92 ;南桥芯片内的端口
45 or al,0000_0010B
46 out 0x92,al ;打开A20
47
48 cli ;中断机制尚未工作
49
50 mov eax,cr0
51 or eax,1
52 mov cr0,eax ;设置PE位
53
54 ;以下进入保护模式... ...
55 jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移
56
57 [bits 32]
58 flush:
59 mov eax,0x0018
60 mov ds,eax
61
62 mov eax,0x0008 ;加载数据段(0..4GB)选择子
63 mov es,eax
64 mov fs,eax
65 mov gs,eax
66
67 mov eax,0x0020 ;0000 0000 0010 0000
68 mov ss,eax
69 xor esp,esp ;ESP <- 0
70
71 mov dword [es:0x0b8000],0x072e0750 ;字符'P'、'.'及其显示属性
72 mov dword [es:0x0b8004],0x072e074d ;字符'M'、'.'及其显示属性
73 mov dword [es:0x0b8008],0x07200720 ;两个空白字符及其显示属性
74 mov dword [es:0x0b800c],0x076b076f ;字符'o'、'k'及其显示属性
75
76 ;开始冒泡排序
77 mov ecx,pgdt-string-1 ;遍历次数=串长度-1
78 @@1:
79 push ecx ;32位模式下的loop使用ecx
80 xor bx,bx ;32位模式下,偏移量可以是16位,也可以
81 @@2: ;是后面的32位
82 mov ax,[string+bx]
83 cmp ah,al ;ah中存放的是源字的高字节
84 jge @@3
85 xchg al,ah
86 mov [string+bx],ax
87 @@3:
88 inc bx
89 loop @@2
90 pop ecx
91 loop @@1
92
93 mov ecx,pgdt-string
94 xor ebx,ebx ;偏移地址是32位的情况
95 @@4: ;32位的偏移具有更大的灵活性
96 mov ah,0x07
97 mov al,[string+ebx]
98 mov [es:0xb80a0+ebx*2],ax ;演示0~4GB寻址。
99 inc ebx
100 loop @@4
101
102 hlt
103
104;-------------------------------------------------------------------------------
105 string db 's0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.'
106;-------------------------------------------------------------------------------
107 pgdt dw 0
108 dd 0x00007e00 ;GDT的物理地址
109;-------------------------------------------------------------------------------
110 times 510-($-$$) db 0
111 db 0x55,0xaa
1.设置堆栈段和栈指针
6 ;设置堆栈段和栈指针
7 mov eax,cs
8 mov ss,eax
9 mov sp,0x7c00
第7、8两行,你可能觉得有点怪异,但是这么写是可以的。关于原因,作者已经在书中说明了。
[bits 16]
mov ds,ax ;8E D8 [bits 32]
mov ds,ax ;66 8E D8 mov ds,eax ;8E D8
以上代码每一行的注释是指令编译后产生的机器码。
对于某些老式的编译器,在编译“mov ds,ax”这条指令时,16位和32位的编译结果是不同的:在32位模式下,会添加前缀0x66(因为编译器认为源操作数AX是16位的,所以要添加0x66以反转默认操作数的大小)。
但是,如果添加了0x66,处理器在执行时就会多花去一个时钟周期,这样的指令又用得很频繁,所以不管是16位还是32位模式,它们被设计为相同的机器指令,都是8ED8,不需要指令前缀。可是某些编译器太固执了,它们依然会加上指令前缀0x66. 好吧,为了照顾它们,程序员想出了一个办法,就是用这样的形式:
mov ds,eax
你别说,还真的有效,果然生成了不加前缀的8ED8!
说到这里,NASM编译器还是非常优秀的,至少他不会那么固执。不管处理器模式怎么变化,也不管指令形式如何,以下代码编译后都是一个结果:
[bits 16]
mov ds,ax ;8E D8
mov ds,eax ;8E D8 [bits 32]
mov ds,ax ;8E D8
mov ds,eax ;8E D8
说了这么多,其实我就是把作者讲的内容又讲了一遍。不管你理解了没有,反正我是有点糊涂了。
因为刚开始的这段代码,是在16位模式下执行的,编译也是按照16位来编译的,所以按照16位的写法就可以了。以下这样写,简单明了。
7 mov ax,cs
8 mov ss,ax
反汇编后,生成的机器码如下:
可是,如果按照配书程序,那么反汇编后成了:
看到了吗,第一行多了前缀0x66,执行时会多用掉一个指令周期。
我个人认为,写代码用通俗的写法就好,能让人看懂的代码才是好代码。OK,这个问题就到这里,我们继续。
2.创建GDT
11 ;计算GDT所在的逻辑段地址
12 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位线性基地址
13 xor edx,edx
14 mov ebx,16
15 div ebx ;分解成16位逻辑地址
16
17 mov ds,eax ;令DS指向该段以进行操作
18 mov ebx,edx ;段内起始偏移地址
106;-------------------------------------------------------------------------------
107 pgdt dw 0
108 dd 0x00007e00 ;GDT的物理地址
109;-------------------------------------------------------------------------------
第12行,就是把GDT的物理地址0x7e00传送到EAX,至于为什么给标号pgdt加上(0x7c00+0x02),相信你已经明白了,如果不明白,看看我的图。
第13行到15行,其实是做除法运算,把物理地址分解为段地址和偏移地址: EDX:EAX / 16 = EAX(得到段地址) …EDX(得到偏移地址)
第17到18行,DS:EBX就指向了GDT的开头。
20 ;创建0#描述符,它是空描述符,这是处理器的要求
21 mov dword [ebx+0x00],0x00000000
22 mov dword [ebx+0x04],0x00000000
23
24 ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
25 mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xfffff
26 mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符
27
28 ;创建2#描述符,保护模式下初始代码段描述符
29 mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,512字节
30 mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符
31
32 ;创建3#描述符,上面代码段的别名描述符
33 mov dword [ebx+0x18],0x7c0001ff ;基地址为0x00007c00,512字节
34 mov dword [ebx+0x1c],0x00409200 ;粒度为1个字节,数据段描述符
第20~30行分别创建了3个描述符,相信大家都很熟悉了。需要说明的是33~34行,创建了一个代码段的别名描述符。这样做用意何在呢?
在保护模式下,代码段是不可写入的,所谓不可写入不是说改变了内存的物理性质,使内存写不进去,而是说通过代码段描述符访问对应的内存区域时,处理器不允许向里面写数据或者更改数据。
但是,如果非要修改代码段,有没有办法呢?有,那就是为该代码段建立一个新描述符,比如说可读可写的数据段描述符,这样,通过这个数据段描述符,我们就可以堂而皇之地修改代码段了。像这样,当两个或以上的描述符都指向同一个段时,把另外的那些描述符就成为别名描述符。
3.栈操作时的保护
36 mov dword [ebx+0x20],0x7c00fffe
37 mov dword [ebx+0x24],0x00cf9600
第36、37行安装了栈段描述符。用我们的小程序分析一下(参见数据段描述符和代码段描述符(二)——《x86汇编语言:从实模式到保护模式》读书笔记11),结果是:
-----------------------
seg_base = 0X7C00
seg_limit = 0XFFFFE
S =
1
DPL = 0
G = 1
D/B = 1
TYPE = 6
数据段: 向下扩展,可读可写
------------------------
得知,基地址是0x7c00,描述符中的界限值是0xFFFFE,G=1,是向下扩展的可读写数据段(一般作为栈段)。
有效界限(effective limit)
段的有效界限取决于G标志。
G=0:有效界限就是描述符中的界限值
G=1:有效界限 = 描述符中的段界限值* 0x1000 +
0xFFF
请牢记这个概念,因为我们会多次用到。
对于下扩(E=1)数据段,有效界限指定了段中最后一个不允许访问的偏移地址。
B=0:偏移地址的有效范围是 [有效界限+1,0xFFFF] ,为了叙述方便,这里用闭区间表示。
B=1:偏移地址的有效范围是 [有效界限+1,0xFFFF_FFFF]
如果要想访问向下扩展的栈段,那么SP或者ESP的值必须要在偏移地址的有效范围内。
结合本文的代码,seg_base = 0X7C00,seg_limit = 0XFFFFE,G = 1,于是有效界限是
0xFFFFE * 0x1000 + 0xFFF = 0xFFFF_EFFF;
那么偏移地址的有效范围是 [ 0xFFFF_F000, 0xFFFF_FFFF]
假设ESP的初始值为0,这时候执行 push eax, 请问合法吗?
分析:ESP先减去4,等于0xFFFF_FFFC,然后(假如合法)EAX的值会被写入 偏移为
0xFFFF_FFFC~0xFFFF_FFFF的四个存储单元,因为这些偏移值在有效范围内,所以没有问题。
假设ESP的初始值为1,这时候执行push eax, 请问合法吗?
分析:ESP先减去4,等于0xFFFF_FFFD,然后(假如合法)EAX的值会被写入 偏移为
0xFFFF_FFFD~0xFFFF_FFFF,0x0000_0000的四个存储单元,因为偏移0不在有效范围内,所以会引发异常。
在Bochs中模拟这种情况,我们发现CPU重启了。
对于POP指令,也是这个道理。
假设ESP的初始值为0xFFFF_FFFC,这时候执行 pop eax, 请问合法吗?
分析:如果合法,那么偏移为
0xFFFF_FFFC~0xFFFF_FFFF的四个存储单元中的内容会传送到eax,之后ESP+4=0;显然0xFFFF_FFFC~0xFFFF_FFFF是有效的偏移,所以允许执行。如下图:
假设ESP的初始值为0xFFFF_FFFD,这时候执行 pop eax, 请问合法吗?
分析:如果合法,那么偏移为
0xFFFF_FFFD~0xFFFF_FFFF,0x0000_0000的四个存储单元中的内容会传送到eax,之后ESP+4=1;显然其中0不是有效的偏移,所以不允许执行。如下图:
再回到我们的代码,因为ESP仅提供偏移地址,真正的物理地址 = 偏移地址 + 段基地址;所以,对于本代码中的栈,结合段基地址=
0x7c00,有效偏移地址= [ 0xFFFF_F000, 0xFFFF_FFFF],所以
最低端有效物理地址 = 0x7c00 + 0xFFFF_F000 = 0x6c00(进位被丢弃)
最高端有效物理地址 = 0x7c00 + 0xFFFF_FFFF = 0x7BFF (进位被丢弃)
也就是说,当前程序定义的栈空间介于物理地址0x6c00~0x7bff 之间。大小为(0x7BFF- 0x6C00 + 0x01 =0x1000
)4KB;
4.修改段寄存器时的保护
54 ;以下进入保护模式... ...
55 jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移
57 [bits 32]
58 flush:
59 mov eax,0x0018
60 mov ds,eax
61
62 mov eax,0x0008 ;加载数据段(0..4GB)选择子
63 mov es,eax
64 mov fs,eax
65 mov gs,eax
66
67 mov eax,0x0020 ;0000 0000 0010 0000
68 mov ss,eax
69 xor esp,esp ;ESP <- 0
第55行,这条指令会隐式地修改CS;同样,会修改寄存器的指令还出现在58~68行(粗体部分)。
以上的指令涉及所有的段寄存器,当这些指令执行时,处理器把指令中给出的选择子传送到段寄存器的选择器部分(就是16位可见部分)。但是,处理器的固件在完成传送之前,会进行如下检查:
(1)检查索引号
要求:段选择子中的描述符索引 * 8 + 7 <= GDT(或LDT)的界限值
如果不符合要求,则产生异常13,同时段寄存器中的原值不变。
(2)检查描述符的类别
原书表12-1,我在这里绘制一份。
Y:表示允许
N:表示不允许
举例:SS只允许加载可读写的数据段。
另外,还需要注意:
- 代码段在任何时候都是不可写的
- 对于DS,ES,FS,GS,可以向其加载数值为0的选择子(但是访问时会导致异常)
- 对于CS和SS,不允许向其传送数值为0的选择子
(3)检查P位
如果P=0,表示描述符指向的段并不存在于物理内存中。此时,处理器中止处理,引发异常。
如果P=1,则处理器将段描述符加载到描述符高速缓存寄存器,同时置A位(仅限于当前讨论的存储器段描述符)
本博文的内容就到这里。第12章余下的内容,请参考存储器的保护(二)——《x86汇编语言:从实模式到保护模式》读书笔记19
存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18的更多相关文章
- ASM:《X86汇编语言-从实模式到保护模式》第10章:32位x86处理器的编程架构
★PART1:32位的x86处理器执行方式和架构 1. 寄存器的拓展(IA-32) 从80386开始,处理器内的寄存器从16位拓展到32位,命名其实就是在前面加上e(Extend)就好了,8个通用寄存 ...
- ASM:《X86汇编语言-从实模式到保护模式》第11章:进入保护模式
★PART1:进入保护模式 1. 全局描述符表(Global Descriptor Table,GDT) 32位保护模式下,如果要使用一个段,必须先登记,登记的信息包括段的起始地址,段的 ...
- ASM:《X86汇编语言-从实模式到保护模式》第8章:实模式下硬盘的访问,程序重定位和加载
第八章是一个非常重要的章节,讲述的是实模式下对硬件的访问(这一节主要讲的是硬盘),还有用户程序重定位的问题.现在整理出来刚好能和保护模式下的用户程序定位作一个对比. ★PART1:用户程序的重 ...
- ASM:《X86汇编语言-从实模式到保护模式》第16章:Intel处理器的分页机制和动态页面分配
第16章讲的是分页机制和动态页面分配的问题,说实话这个一开始接触是会把人绕晕的,但是这个的确太重要了,有了分页机制内存管理就变得很简单,而且能直接实现平坦模式. ★PART1:Intel X86基础分 ...
- ASM:《X86汇编语言-从实模式到保护模式》第12章:存储器的保护
12章其实是11章的拓展,代码基本不变,就是在保护模式下展开讨论. ★PART1:存储器的保护机制 1. 修改段寄存器的保护 当执行把段选择子传到段寄存器的选择器部分的时候,处理器固件在完成传送之前, ...
- ASM:《X86汇编语言-从实模式到保护模式》第15章:任务切换
15章其实应该是和14章相辅相成的(感觉应该是作者觉得14章内容太多了然后切出来了一点).任务切换和14章的某些概念是分不开的. ★PART1:任务门与任务切换的方法 1. 任务管理程序 14章的时候 ...
- ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟
中断是处理器一个非常重要的工作机制.第9章是讲中断在实模式下如何工作,第17章是讲中断在保护模式下如何工作. ★PART1:外部硬件中断 外部硬件中断是通过两个信号线引入处理器内部的,这两条线分别叫N ...
- ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述
★PART1:32位保护模式下任务的隔离和特权级保护 这一章是全书的重点之一,这一张必须要理解特权级(包括CPL,RPL和DPL的含义)是什么,调用门的使用,还有LDT和TSS的工作原理(15章着重 ...
- ASM:《X86汇编语言-从实模式到保护模式》5-7章:汇编基础
第5-7章感觉是这一本书中比较奇怪的章节,可能是作者考虑到读者人群水平的差异,故意由浅入深地讲如何在屏幕上显示字符和使用mov,jmp指令等等,但是这样讲的东西有点重复,而且看了第六,第七章以后,感觉 ...
随机推荐
- EasyUI combobox动态增加选择项
有需求需要动态的为combobox增加可选项,后来解决方案如下 html如下 <select id="workerList"></select> js 如下 ...
- 利用Trace.WriteLine定位难以重现的问题
最近的一个项目中,在客户测试环境(UAT)发现了一个bug,却反复尝试都无法在开发环境和QA环境来重现.界面上也没有出现任何异常和错误,只是某个数据的显示错误,其他数据都正常.仔细分析和调试了出错位置 ...
- 学习笔记之Struts2—浅析接收参数
最近自己通过视频与相关书籍的学习,对action里面接收参数做一些总结与自己的理解. 0.0.接收参数的(主要)方法 使用Action的属性接收参数 使用DomainModel接收参数 使用Mod ...
- SSH密钥登陆
参考: SSH公钥登录原理 比如git可以生成公钥,然后用有权限的账户把他加到仓库上,以后就可以通过公钥登陆了.不需要像https那样需要有账号,但是权限管理就不细了. 有时候如果仓库上添加了多个公钥 ...
- ANE-IOS与AS的互通
从AS调用IOS的函数与传参数 extContex是ExtensionContext的实例,通过call调用.第一个参数是IOS对应的函数,之后所有的参数会变成一个数组传入IOS中.也就是说函数名称后 ...
- django中models联合唯一unique_together
例: 文章点赞 class ArticleUpDown(models.Model): """ 点赞表 """ nid = models.Au ...
- [Android] Android MVP 架构下 最简单的 代码实现
Android MVP 架构下 最简单的 代码实现 首先看图: 上图是MVP,下图是MVC MVP和MVC的区别,在于以前的View层不仅要和model层交互,还要和controller层交互.而 ...
- VMware安装linux系统报错:已将该虚拟机配置为使用 64 位客户机操作系统。但是,无法执行 64 位操作。
检测问题所在: 下载LeoMoon CPU-V 检查一下CPU VT-x状态是否启用 地址:http://download.csdn.net/detail/qq_22860341/9858011 如果 ...
- [Objective-C语言教程]块(12)
Objective-C类定义了一个将数据与相关行为相结合的对象. 有时,仅表示单个任务或行为单元而不是方法集合是有意义的. 块是C,Objective-C和C++等编程语言中的高级功能,它允许创建不同 ...
- short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1;有什么错?
对于 short s1 = 1; s1 = s1 + 1;由于 s1+1运算时会自动提升表达式的类型,所以结果是 int型,再赋值给 short 类型 s1时, 编译器将报告需要强制转换类型的错误.对 ...