第1章 背 景 知 识
让我们在轻松的背景知识介绍中开始Win32汇编之旅。本章将对Win32平台的历史和现状做简要介绍,同时对80386处理器以及Windows操作系统中涉及Win32汇编的基础知识部分做快速充电。
1.1 Win32的软硬件平台
1.1.1 80x86系列处理器简史
Win32可以在多种硬件平台上运行,但使用最广泛的硬件平台是基于Intel公司80x86系列处理器的微型计算机。
自1978年6月Intel公司推出它的第一个16位微处理器8086以来,计算机技术就开始进入飞速发展的时期。8086芯片的主频为4.43 MHz,集成的晶体管数大约为2.9万个,运算器的位长为16位,采用了20条地址线,可以寻址的范围为220个字节地址,即1 MB;1982
年,该公司发布了80286处理器,芯片上集成了12万个晶体管,主频提高到了12 MHz。
1985年Intel公司推出32位的80386处理器,芯片上集成的晶体管数为27.5万个,主频提高到了33 MHz,地址线则扩展为32条,直接寻址的能力达到4 GB。80386处理器在设计的时候考虑了多用户及多任务的需要,在芯片中增加了保护模式、优先级、任务切换和片内的存储单元管理等硬件单元。80386的出现使Windows和UNIX等多任务的操作系统可以在
PC上运行。直到现在,运行于80x86处理器之上的多任务操作系统都是以80386的运行模式为基础的。
1989年,Intel公司推出80486处理器,在芯片内集成了浮点处理器和8 KB的一级缓存,
片内的晶体管数达到了118万个,并把主频提高到50 MHz~66 MHz。80486处理器开始使用流水线技术,即在CPU中由5~6个不同功能的电路单元组成一条指令处理流水线,然后将一条指令分成5~6步后再由这些电路单元分别执行,由此提高CPU的运算速度。电路单元的数目就是流水线的深度。为了使计算机中的其他部件不至于成为CPU速度发展的瓶颈,
80486处理器开始使用了倍频技术,即让处理器速度(CPU主频)数倍于系统总线速度(外频)。
从80386开始,在Intel公司向市场大量推出处理器芯片的同时,其他一些电脑公司和厂商如AMD和Cyrix等,也纷纷投入大量的人力财力进行处理器的开发和研制,并很快把研制出的产品推向市场。这些CPU芯片和80386芯片兼容,在编程上可以使用和Intel处理器相同的指令集。
1993年3月Intel公司推出80586处理器。由于无法阻止其他公司把自己的兼容产品也叫做x86,所以把产品取名为Pentium,并且进行了商标注册,同时启用了中文名称null奔腾null。
Pentium芯片中集成了310万个晶体管,内置16 KB缓存,主频有60 MHz和66 MHz两个版
本,后来逐步提高,到1995年6月时主频提高到了133 MHz。Pentium处理器采用许多新技术,其中最重要的变化是采用了超标量体系结构。即将两个同时工作的指令执行部件封装在同一芯片中,用两条并行的通道来执行指令,这相当于两个CPU同时工作,大大提高了处理速度。在586时代,AMD和Cyrix等其他公司也推出了相应档次的CPU,命名为5x86和
K5等。
1995年11月,Intel公司推出代号为P6的新一代Pentium Pro处理器,中文名称为null高能奔腾null。Pentium Pro芯片中集成了550万个晶体管,主频分150 MHz~200 MHz多个版本。
片内集成了3条平行的指令执行通道,相当于3个CPU并行工作,并用超流水线技术将流水线的深度提高到了14级。P6处理器内置16 KB一级高速缓存,并将256 KB或512 KB的二级高速缓存芯片与CPU内核芯片同时封装在一个外壳中,缩短了CPU和二级高速缓存之间的线路走线距离。同时,P6处理器开始使用乱序执行和分支预测技术,这使下一条指令不一定要等到前一条指令执行完毕后才可以开始。所有这些技术使这种CPU在运行32位指令系统时的执行效率明显高于上一代Pentium。
随着CPU和操作系统的发展,多媒体技术开始流行,表明依靠浮点处理器已经不能满足多媒体音频和视频信号的实时处理任务了。1997年初,Intel公司在Pentium Pro芯片上增加了专用于多媒体处理的57条指令和8个64位专用寄存器,命名为Pentium MMX。Pentium
MMX使用了450万个晶体管,最高主频达到了233 MHz。
1997年5月,Intel公司又向市场推出了Pentium II芯片,中文名称为null奔腾II代null。Pentium
II内集成了750万个晶体管,最高主频达到了300 MHz,也具有MMX的功能。这种处理器将二级高速缓存移到芯片外,以提高芯片成品率。为了照顾低端市场,1998年Intel公司推出了除去二级高速缓存的Pentium II简化版,命令为Celeron处理器。由于其缺乏片内二级高速缓存,对速度的影响非常巨大,使Celeron处理器的实际性能非常低。1998年4月,Intel
公司又把128 KB二级高速缓存加回到Celeron处理器中,命名为Celeron A处理器。Celeron
A的主频从300 MHz开始。
1999年,Intel公司推出集成了950万个晶体管,主频为450 MHz~500 MHz,外频为100
MHz的Pentium III处理器。这种处理器新增了SSE指令集,提供70条全新的指令,可以大大提高3D运算、动画片、影像与音效等功能,增强了视频处理和语音识别的功能。这套指令集主要为浏览WWW网页而设计。Pentium III处理器在芯片内集成了64 KB的一级缓存,
并将512 KB的二级缓存安装在外壳卡盒内。
2000年11月,Intel公司发布集成4 200万个晶体管的Pentium 4处理器,主频达到了1.4
GHz,系统总线速度为400 MHz,流水线的深度提高到20级,并增加了SSE2指令集,提供
144条新指令用于提高摄像、多媒体、3D图像和密集运算等方面的速度。时至今日,Intel
公司已经发布了主频超过2 GHz的Pentium 4处理器。
从第一块微处理器诞生至今,处理器技术发展出不少新的体系结构。从微处理器的指令系统来看,有两种分支走向,一种是CISC;一种是RISC。CISC即复杂指令系统计算机。从
PC诞生以来,人们一直沿用CISC指令集方式。它的指令不等长,指令的条数比较多,编程和设计处理器时都较为麻烦。在CISC之后,人们发明了RISC,即精简指令系统。这种指令系统采用等长的指令,且指令数较少,通过简化指令可以让计算机的结构更为简单,进而提高运算速度。
Intel的80x86系列处理器看起来属于CISC体系,但实际上,从Pentium处理器开始,
都已不是单纯的CISC体系了。因为它们引入了很多RISC体系里的先进技术来大幅度提高性能。但是,好马也得配好鞍nullnull没有软件支持的CPU再快也不是好CPU。为了兼容已有的软件,80x86系列处理器也不得不背上沉重的历史包袱。如CPU的位长还是停留在32位;
在寄存器、运行模式与内存管理模式等方面还是继承了早期的80386模式;80386以后的处理器虽然增加了不少新指令,但大多用于多媒体扩展,其中很少有和操作系统密切相关的指令。所以,如果不涉及3D及密集运算方面的运算,仅从操作系统的角度看,这些处理器只能算是一个快速的80386处理器而已。
1.1.2 Windows的历史
Win32指的是32位的Windows系操作系统。Microsoft公司有一系列的Windows操作系统,下面先简单介绍Windows的历史。
谈到Windows的历史就不能不谈MS-DOS的历史。MS-DOS的技术源自CP/M操作系统。1973年,第一个8位磁盘操作系统CP/M出现,这种操作系统有较好的层次结构,它利用BIOS隔离硬件和操作系统的其他模块,有很好的可移植性和易用性。在此基础上,西雅图计算机公司于1978年开始开发QDOS,此后又成功研制出16位微型机的实验性操作系统
86-DOS。
也正是在这段时期,IBM公司正在开发基于8086处理器的IBM PC,急需一个配套的操作系统,但和CP/M开发者之间的谈判不是很顺利,于是Microsoft乘虚而入。Microsoft没有足够的时间开发新的操作系统,于是找到了西雅图计算机公司,双方达成了由Microsoft
经销86-DOS操作系统的协议。以86-DOS操作系统为基础,Microsoft很快开发出MS-DOS 1.0
版本。1981年8月,MS-DOS 1.0和IBM PC一起发布。
MS-DOS 1.0还不支持硬盘和分层目录结构,文件管理中继承了CP/M操作系统的许多功能,但仅支持单面软盘。到了1983年,为了支持带硬盘的PC/XT计算机,经过较大地改造并吸取了UNIX的很多优点后,MS-DOS升级到2.0版本,可以支持32 MB大小的硬盘分区。
1984年,MS-DOS升级到3.0版本,开始支持1.2 MB软盘,用于PC/AT计算机。1986年,
为了支持3.5英寸软盘,MS-DOS升级到3.2版本。
1987年,为了兼容IBM和PS/2个人计算机,MS-DOS升级到3.3版,这也是最流行的
DOS版本。1990年,Microsoft推出MS-DOS 5.0,开始支持2.88 MB的软盘,并可以把部分系统代码放到高端内存运行,空出低端内存供应用程序使用,同时将磁盘单个分区的支持容量提高到了2 GB。
一直到MS-DOS的最后版本6.22为止,绝大多数的PC上运行的就是这个字符界面的操作系统。当时要想玩转DOS,必须有专业计算机知识,不然nullBad command or filenamenull之类的提示随处可见,对此一般用户还真会不知所措。所以,虽然null界面简陋却令人兴奋null的
Windows 1.0于1985年11月正式发布时,还是为沉闷的屏幕带来了一丝清新,毕竟它使非专业的人员使用计算机变得容易。在增强了键盘和鼠标接口后,1987年微软又推出了
Windows 2.0版。由于当时的硬件和DOS功能的限制,Windows并不实用,所以这两个版本并不成功。Windows 2.0版发布不久,Intel公司的80386处理器发布,Microsoft推出使用80386
处理器V86模式的Windows 2.1,即Windows/286。
在接下来的时间里,基于Intel 80x86微处理器的IBM兼容机已经快速普及,这给
Microsoft开发新的Windows系统提供了发展空间和市场。Microsoft公司对Windows的内存
管理和图形界面做了重大改进,在1990年5月份推出了Windows 3.0,可以支持Intel
80286/386/486微处理器的保护模式,并可以访问达16 MB的内存。Windows 3.0一面世便在商业上取得了惊人的成功,从而一举奠定了Microsoft在操作系统上的垄断地位。1992年4
月,Microsoft推出了更稳定的Windows 3.1,可以支持True Type字体。Windows 3.1是16位
Windows中最流行的版本。
1993年5月,Microsoft发布了具备安全性和稳定性特征的32位操作系统Windows NT
3.11,主要针对网络和服务器市场。nullNTnull代表null新技术null(New Technology)。NT 3.11是
Windows系列中使用32位编程模式的第一个版本。它充分利用80386及以上处理器的平面地址空间和保护模式等新技术,并可以移植到Alpha,MIPS和Power PC等不同的处理器平台上运行。
随后,Microsoft借Windows东风,于1995年8月推出新一代操作系统Windows 95(又名Chicago)。Windows 95实现了很友好的用户界面,支持即插即用功能,支持主流多媒体设备和DirectX编程接口,成为Microsoft发展史上的一个里程碑,也是操作系统发展史上的一个里程碑。从此,Windows 9x便取代了Windows 3.x和MS-DOS操作系统,成为个人计算机平台的主流操作系统。
1998年Microsoft又发布了使用更方便的Windows 98。本来Windows 98是Microsoft最后一款基于Win 9x内核的操作系统,并打算将下一代的操作系统建立在更加稳定的Windows
NT内核上,也就是Windows NT 5.0。但是在开发过程中,Microsoft意识到基于NT内核的系统不可能提供足够的多媒体支持,于是继续开发了基于Win 9x内核的Windows Me。虽然其性能并不比Windows 98有很大提高,但在操作界面上有了更人性化的改进。
在操作系统的分类上,Microsoft根据家庭个人用户和商业办公用户的不同需求,分别提供Window 9x和Windows NT系列,Windows 9x注重用户界面及其他易用性特征,而NT系列则在纯 32 位内核的稳定性和可靠性等企业级特征上下功夫;另一方面,特别针对不同规模商业用户的需求,Windows NT系列分为工作站版和服务器版等多个版本。在Windows 9x系列上,从Windows 95 OSR2版起,Microsoft先后发布了Windows 98,Windows 98 SE和
Windows Me 3个面向家庭和个人用户的PC操作系统,并把IE浏览器集成在PC操作系统中;
而在商用操作系统领域,继Windows NT 3.11之后,Microsoft相继发布了Windows NT 3.5和
4.0两代操作系统,并在NT 4.0上采用了Windows 95式的用户界面。2000年,微软发布采用纯32位内核并照顾了家庭消费类应用软件的Windows NT 5.0,即Windows 2000。至此,
Microsoft的两个系列操作系统终于开始统一。
为了利用MS-DOS时代大量的应用程序,保持向下的兼容性,Windows 9x的内核模块还有许多地方使用16位程序,但在编程上,支持32位的编程模式。Windows NT系列和
Windows 9x系列操作系统都支持Win32 API(Application Programming Interface),即Windows
32位应用程序编程接口,Win32 API为应用程序提供了大量的系统功能调用,通过Win32 API
调用Windows系统相当于在MS-DOS中通过中断方式调用系统功能。就像DOS汇编程序中随处可见的INT 21h指令一样,Windows应用程序中Win32 API也随处可见。
1.1.3 Win32平台的背后nullnullWintel联盟
Windows是伴随着Intel 80x86系列处理器从弱小逐渐成为霸主的。在20世纪90年代,
Intel 80x86系列处理器更新换代最快的时期也就是Microsoft的Windows系列最红的时期。
在这个时期,Windows标志和Intel Inside标志几乎是每一台桌面PC上都有的烙印。Microsoft
和Intel公司一软一硬,完全统治着全球PC机的市场,成为整个PC时代的象征,被世人称为Wintel联盟。
从20世纪80年代起,当时规模甚小的Microsoft和Intel正式携手,逐步垄断了计算机产业硬件与软件的主要领域。每当Microsoft推出功能更强的软件后,Intel处理器的需求量就上升;同样,当Intel生产出速度更快的处理器后,Microsoft的软件因有了更好的平台而显得更易用。Intel有多快的CPU,Microsoft就有相应的、庞大的软件来支持它。Microsoft的应用程序不管有多庞大,需要多快的速度,Intel的新处理器又总能满足它。业界也必须出奇一致地放弃原有的软硬件平台,转到新平台上去。因为,谁跟不上Wintel的步伐,谁就极有可能被淘汰出局。
Wintel联盟不仅是针对竞争对手的联盟,它还是迫使用户升级的同盟。在升级的循环中,
多数用户往往为了一个应用而被迫升级整个系统。Intel有多快的CPU,Microsoft 就有多花哨、多庞大的操作系统与之相配合。操作系统的升级即意味着应用软件的全面升级,而应用软件的升级则意味着用户整个系统必须升级。如果用户还在原有的系统上工作,那么就再也得不到新软件的支持了,因为,所有的应用软件公司都不愿意在过时的操作系统上投资开发自己的应用软件。大家都有体会,运行Windows 95很快的Pentium II/250把Windows 98一装上去,立刻慢了下来,等到升级到了Pentium III/450,Windows 98运行起来很快了,Windows
2000又出来了,null快速null的PIII又成了老牛。在又一轮的升级下,CPU爬到了1 GHz以上,
等到Windows 2000运行起来很舒畅了,再试一下Windows XP,用户升级的欲望又出来了!
让客户口袋里的银子永远不会有满的一天。
时钟走过2000年,Wintel联盟已呈衰减之势。Sun,IBM,Oracle,Linux,垄断和司法部等名词让Microsoft感到头痛;AMD的速龙和钻龙处理器也让Intel手忙脚乱。Microsoft
和Intel在利益上的冲突也越来越公开化,整个业界都感受到了Wintel联盟将土崩瓦解的气息。
不管业界风起云涌也好,一片死寂也好,Intel 80x86平台和Windows是桌面计算机上最流行的配置已是不争的事实。为了自由和创新,我们可以去学习Linux,但在更多的时候,
学会Win32编程是不得已的选择,即使是全世界的计算机中只剩50%在运行Windows。
Windows程序员仍然有广阔的用武之地,这也算是无奈之际给自己一个理由吧!
1.2 Windows的特色
对于使用者来说,关于Windows的特色毋须多言,下面的几个特点足以使它广泛流行,
● 图形用户界面(GUI,Graphic User Interface 的缩写词) Windows最重要的特色,
用户由此摆脱了字符界面操作系统必须死记的键盘命令和令人一头雾水的屏幕提示,改为以鼠标为主可以直接和屏幕上所见即所得的东西进行交流。
● 一致的用户界面 使初学者便于使用,大部分的 Windows 程序界面看起来都是差不多的,都有菜单和标题栏等,掌握一个程序后就很容易尝试新的程序。
● 多任务 是非常重要的特色,用户可以同时运行多个程序,一边工作一边听 MP3
显然是很吸引人的。另一个好处是用户可以在不同的程序之间传送数据。
但对于程序员来说,更关心隐藏在底下的东西,Windows究竟提供了什么便利?用Win32
开发程序方便吗?对程序员来说,更关心Windows的以下特征,
● 大量的函数调用nullnullWin32 支持上千种函数的调用,几乎涉及所有的方面,程序员可以把更多的时间放在程序的逻辑结构和用户界面上。
● 和设备的无关性nullnull Win32 程序并不直接访问屏幕、打印机和键盘等硬件设备,
Windows虚拟了所有的硬件。只要有硬件的设备驱动程序,这个硬件就可以使用,应用程序并不需要关心硬件的具体型号。与DOS编程中需要针对不同的显示卡和打印机等编写很多的驱动程序来比,这个特性对程序员的帮助是巨大的。
● 内存管理nullnull由于内存分页和虚拟内存的使用,每个程序都可以使用 4 GB 的地址空间,DOS编程时必须考虑的640 KB内存问题已经成为历史。
1.3 必须了解的东西
1.3.1 80x86处理器的工作模式
80386处理器有3种工作模式:实模式、保护模式和虚拟86模式。实模式和虚拟86模式是为了和8086处理器兼容而设置的。在实模式下,80386处理器就相当于一个快速的8086
处理器。保护模式是80386处理器的主要工作模式。在此方式下,80386可以寻址4 GB的地址空间,同时,保护模式提供了80386先进的多任务、内存分页管理和优先级保护等机制。
为了在保护模式下继续提供和8086处理器的兼容,80386又设计了一种虚拟86模式,以便可以在保护模式的多任务条件下,有的任务运行32位程序,有的任务运行MS-DOS程序。
在虚拟86模式下,同样支持任务切换、内存分页管理和优先级,但内存的寻址方式和8086
相同,也是可以寻址1 MB的空间。
由此可见,80386处理器的3种工作模式各有特点且相互联系。实模式是80386处理器工作的基础,这时80386当做一个快速的8086处理器工作。在实模式下可以通过指令切换到保护模式,也可以从保护模式退回到实模式。虚拟86模式则以保护模式为基础,在保护模式和虚拟86模式之间可以互相切换,但不能从实模式直接进入虚拟86模式或从虚拟86模式直接退到实模式。
1,实模式
80386处理器被复位或加电的时候以实模式启动。这时候处理器中的各寄存器以实模式的初始化值工作。80386处理器在实模式下的存储器寻址方式和8086是一样的,由段寄存器的内容乘以16当做基地址,加上段内的偏移地址形成最终的物理地址,这时候它的32位地址线只使用了低20位。在实模式下,80386处理器不能对内存进行分页管理,所以指令寻址的地址就是内存中实际的物理地址。在实模式下,所有的段都是可以读、写和执行的。
实模式下80386不支持优先级,所有的指令相当于工作在特权级(优先级0),所以它可以执行所有特权指令,包括读写控制寄存器CR0等。实际上,80386就是通过在实模式下初始化控制寄存器,GDTR,LDTR,IDTR与TR等管理寄存器以及页表,然后再通过加载CR0
使其中的保护模式使能位置位而进入保护模式的。实模式下不支持硬件上的多任务切换。
实模式下的中断处理方式和8086处理器相同,也用中断向量表来定位中断服务程序地址。中断向量表的结构也和8086处理器一样,每4个字节组成一个中断向量,其中包括两个字节的段地址和两个字节的偏移地址。
从编程的角度看,除了可以访问80386新增的一些寄存器外,实模式的80386处理器和
8086有什么进步呢?其实最大的好处是可以使用80386的32位寄存器,用32位的寄存器进行编程可以使计算程序更加简捷,加快了执行速度。比如在8086时代用16位寄存器来完成
32位的乘法和除法时,要进行的步骤实在是太多了,于是考试时出这一类的题目就成了老师们的最爱,所以那时候当学生做梦都想着让寄存器的位数快快长,现在梦想终于成真了,用
32位寄存器一条指令就可以完成(问题是老师们也发现了这个投机取巧的办法,为了达到让学生们基础扎实的目的,也把题目换成了64位的乘法和除法,所以现在晚上做的梦换成了寄存器忽然长到了64位);其次,80386中增加的两个辅助段寄存器FS和GS在实模式下也可以使用,这样,同时可以访问的段达到了6个而不必考虑重新装入的问题;最后,很多80386
的新增指令也使一些原来不很方便的操作得以简化,如80386中可以使用下述指令进行数组访问,
mov cx,[eax + ebx * 2 + 数组基地址]
这相当于把数组中下标为eax和ebx的项目放入cx中;ebx * 2中的2可以是1,2,4
或8,这样就可以支持8位到64位的数组。而在8086处理器中,实现相同的功能要进行一次乘法和两次加法。另外,pushad和popad指令可以一次把所有8个通用寄存器的值压入或从堆栈中弹出,比起用下面的指令分别将8个寄存器入栈要快了很多,
push eax
push ebx
,.,
pop ebx
pop eax
当然,使用了这些新指令的程序是无法拿回到8086处理器上去执行的,因为这些指令的编码在8086处理器上是未定义的。
2,保护模式
当80386工作在保护模式下的时候,它的所有功能都是可用的。这时80386所有的32
根地址线都可供寻址,物理寻址空间高达4 GB。在保护模式下,支持内存分页机制,提供了对虚拟内存的良好支持。虽然与8086可寻址的1 MB物理地址空间相比,80386可寻址的物理地址空间可谓很大,但实际的微机系统不可能安装如此大的物理内存。所以,为了运行大型程序和真正实现多任务,虚拟内存是一种必需的技术。
保护模式下80386支持多任务,可以依靠硬件仅在一条指令中实现任务切换。任务环境的保护工作是由处理器自动完成的。在保护模式下,80386处理器还支持优先级机制,不同的程序可以运行在不同的优先级上。优先级一共分0~3 4个级别,操作系统运行在最高的优先级0上,应用程序则运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好地隔离各个任务。从实模式切换到保护模式是通过修改控制寄存器CR0的控制位PE(位0)来实现的。在这之前还需要建立保护模式必需的一些数据表,
如全局描述符表GDT和中断描述符表IDT等。
DOS操作系统运行于实模式下,而Windows操作系统运行于保护模式下。
3,虚拟86模式
虚拟86模式是为了在保护模式下执行8086程序而设置的。虽然80386处理器已经提供了实模式来兼容8086程序,但这时8086程序实际上只是运行得快了一点,对CPU的资源还是独占的。在保护模式的多任务环境下运行这些程序时,它们中的很多指令和保护模式环境格格不入,如段寻址方式、对中断的处理和I/O操作的特权问题等。为了在保护模式下工作而丢弃这些程序的代价是巨大的。设想一下,如果Windows或80386处理器推出的时候宣布不能运行以前的MS-DOS程序,那么就等于放弃了一个巨大的软件库,Windows以及80386
处理器可能就会落得和苹果机一样的下场,这是Microsoft和Intel都不愿看到的。所以,80386
处理器又设计了一个虚拟86模式。
虚拟86模式是以任务形式在保护模式上执行的,在80386上可以同时支持由多个真正的
80386任务和虚拟86模式构成的任务。在虚拟86模式下,80386支持任务切换和内存分页。
在Windows操作系统中,有一部分程序专门用来管理虚拟86模式的任务,称为虚拟86管理程序。
既然虚拟86模式以保护模式为基础,它的工作方式实际上是实模式和保护模式的混合。
为了和8086程序的寻址方式兼容,虚拟86模式采用和8086一样的寻址方式,即用段寄存器乘以16当做基址再配合偏移地址形成线性地址,寻址空间为1 MB。但显然多个虚拟86任务不能同时使用同一位置的1 MB地址空间,否则会引起冲突。操作系统利用分页机制将不同虚拟86任务的地址空间映射到不同的物理地址上去,这样每个虚拟86任务看起来都认为自己在使用0~1 MB的地址空间。
8086代码中有相当一部分指令在保护模式下属于特权指令,如屏蔽中断的cli和中断返回指令iret等。这些指令在8086程序中是合法的。如果不让这些指令执行,8086代码就无法工作。为了解决这个问题,虚拟86管理程序采用模拟的方法来完成这些指令。这些特权指令执行的时候引起了保护异常。虚拟86管理程序在异常处理程序中检查产生异常的指令,如果是中断指令,则从虚拟86任务的中断向量表中取出中断处理程序的入口地址,并将控制转移过去;如果是危及操作系统的指令,如cli等,则简单地忽略这些指令,在异常处理程序返回的时候直接返回到下一条指令。通过这些措施,8086程序既可以正常地运行下去,在执行这些指令的时候又觉察不到已经被虚拟86管理程序做了手脚。MS-DOS应用程序在
Windows操作系统中就是这样工作的。
1.3.2 Windows的内存管理
在这一节中,读者可以解决初学Win32汇编的两个大疑问,
1,Win32 汇编中,每个程序都可以用 4 GB 的内存吗?
2,Win32 汇编源代码中为什么看不到 CS,DS,ES 和 SS 等段寄存器的使用?
1,DOS操作系统的内存安排
Win32编程相对于DOS编程最大的区别之一就是内存的使用。
先来回顾一下DOS操作系统的内存使用,如图1.1所示。DOS操作系统运行于实模式中,由于8086处理器的寻址范围只有1 MB,当时把系统硬件使用的内存安排在高端,地址是从A0000h(即640 KB)开始的384 KB中,其中有用于显示的视频缓冲区和BIOS的地址
空间。而在内存低端,安排了中断向量表和
BIOS数据区;剩下从500h开始到A0000h总共不到640 KB的内存是操作系统和应用程序所能够使用的;应用程序不可能使用这640 KB
以外的内存。这就是著名的null640KB限制null。
但即使在这640 KB中,DOS操作系统又占领了低端的一部分内存,最后剩下600 KB左右的内存才是应用程序真正可以用的。如果系统中有内存驻留程序存在,那么应用程序还要和这些TSR程序共同分享这段内存空间。
当80386处理器推出后,可以寻址的内存范围达到了4 GB,利用XMS驱动程序可以访问到所有的4 GB地址空间。但16位的段寻址方式限制了DOS程序,null可见null的内存范围还是停留在00000h到FFFF0h+64 KB的范围内,所有高于1 MB的扩展内存只能通过XMS驱动程序当做数据交换使用,程序的执行空间并没有什么增加。
2,80386的内存寻址机制
Windows的内存管理和DOS的内存管理有很大的不同,在了解Windows的内存管理模式之前,需要对80386保护模式下内存分页机制有些了解。为了做个对比,先来看实模式下的内存寻址方式,即DOS下的寻址方式,如图1.2所示。
64KB
fffffh(1MB)
xxxx0h
00000h
物理内存
16位的
段寄存器
x10h
0000h
yyyyh
ffffh
地址xxxx:yyyy
xxxx
图1.2 实模式下的内存寻址方式
在实模式下,一个完整的地址由段地址和偏移地址两部分组成。段地址放在16位的段寄存器中,然后在指令中用16位的偏移地址寻址。处理器换算时先将段地址乘以10h,得到段在物理内存中的起始地址;然后加上16位的偏移地址得到实际的物理地址。如xxxx:yyyy格式的虚拟地址在内存中的实际位置是xxxxnull10h+yyyy。
当80386处理器工作在保护模式和虚拟8086模式的时候,可以使用全部32根地址线访
64KB高端内存
ROM扩展、系统BIOS地址
VGA BIOS地址
彩色字符模式视频缓冲区
单色字符模式视频缓冲区
图形模式视频缓冲区
可用空间
系统程序
(DOS的驻留部分、驱动程序等)
DOS数据区
BIOS数据区
中断向量表 00000h
00400h
00500h
A0000h
B0000h
B8000h
C0000h
C8000h
FFFFFh
图1.1 DOS操作系统的内存安排
问4 GB大的内存。段地址加偏移地址的计算方法显然无法覆盖这么大的范围。但计算一下就可以发现,实际上和8086同样的限制已经不复存在,因为80386所有的通用寄存器都是
32位的,2的32次方相当于4G,所以用任何一个通用寄存器来间接寻址,不必分段就已经可以访问到所有的内存地址。
这是不是说,在保护模式下,段寄存器就不再有用了呢?答案是否定的。实际上段寄存器更有用了,虽然在寻址上不再有分段的限制问题,但在保护模式下,一个地址空间是否可以被写入,可以被多少优先级的代码写入,是不是允许执行等涉及保护的问题就出来了。要解决这些问题,必须对一个地址空间定义一些安全上的属性。段寄存器这时就派上了用途。
但是涉及属性和保护模式下段的其他参数,要表示的信息太多了,要用64位长的数据才能表示。我们把这64位的属性数据叫做段描述符(Segment Descriptor)。
80386的段寄存器是16位的,无法放下保护模式下64位的段描述符。如何解决这个新的问题呢?解决办法是把所有段的段描述符顺序放在内存中的指定位置,组成一个段描述符表(Descriptor Table);而段寄存器中的16位用来做索引信息,指定这个段的属性用段描述符表中的第几个描述符来表示。这时,段寄存器中的信息不再是段地址了,而是段选择器
(Segment Selector)。可以通过它在段描述符表中null选择null一个项目以得到段的全部信息。
既然这样,段描述符表放在那里呢?80386中引入了两个新的寄存器来管理段描述符表。
一个是48位的全局描述符表寄存器GDTR,一个是16位的局部描述符表寄存器LDTR。那么,为什么有两个描述符表寄存器呢?
GDTR指向的描述符表为全局描述符表GDT(Global Descriptor Table)。它包含系统中所有任务都可用的段描述符,通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符及各任务的LDT段等;全局描述符表只有一个。
LDTR则指向局部描述符表LDT(Local Descriptor Table)。80386处理器设计成每个任务都有一个独立的LDT。它包含有每个任务私有的代码段、数据段和堆栈段的描述符,也包含该任务所使用的一些门描述符,如任务门和调用门描述符等。
不同任务的局部描述符表分别组成不同的内存段,描述这些内存段的描述符当做系统描述符放在全局描述符表中。和GDTR直接指向内存地址不同,LDTR和CS,DS等段选择器一样只存放索引值,指向局部描述符表内存段对应的描述符在全局描述符表中的位置。随着任务的切换,只要改变LDTR的值,系统当前的局部描述符表LDT也随之切换,这样便于各任务之间数据的隔离。但GDT并不随着任务的切换而切换。
看到这里,读者可能会提出一个问题,既然有全局描述符表和局部描述符表两个表,那么段选择器中的索引值对应哪个表中的描述符呢。实际上,16位的段选择器中只有高13位表示索引值。剩下的3个数据位中,第0,1位表示程序的当前优先级RPL;第2位TI位用来表示在段描述符的位置;TI=0表示在GDT中,TI=1表示在LDT中。
以图1.3为例,在保护模式下,同样以xxxx:yyyyyyyy格式表示一个虚拟地址。单单凭段选择器中的数值xxxx根本无法反映出段的基址在哪里。对于这个地址,首先要看xxxx的
TI位是否为0,如果是的话,则先从GDTR寄存器中获取GDT的基址(图中的步骤①),然后在GDT中以段选择器xxxx的高13位当做位置索引得到段描述符(步骤②)。段描述符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(步骤③);如果xxxx的
TI位为1的话就更复杂了,这表示段描述符在LDT中,这时第一步的操作还是从GDTR寄存器中获取GDT的基址(步骤○1'),并且要从LDTR中获取LDT所在段的位置索引(步骤
○2');然后以这个位置索引在GDT中得到LDT段的位置(步骤○3');然后才是用xxxx做索引从LDT段中获得段描述符(步骤○4'),再以这个段描述符得到段的基址等信息(步骤○5')。
分这两种情况得到段的基址后(图中Result所示),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
关于段描述符的格式定义,读者可以参考其他讲述保护模式的书籍。
47 15 0
基址和限长在
描述符中定义
内存线性地址
00000000h
ffffffffh GDT 描述符0
描述符1
LDT1描述符
LDT2描述符
描述符x
描述符0
描述符1
描述符x
描述符x
对应的
内存段
LDT
GDT
GDTR 基址 限长
LDT
TI位
索引值x
索引值n
TI=0
TI=1
LDTR
CS DS
ES FS
GS SS
15 3 0
15 3 0
0
1
0
1
n+1
n
x
x
图1.3 保护模式下GDTR,LDTR,全局描述符表,局部描述符表和选择器的关系
3,80386的内存分页机制
读者可以注意到,在实模式下寻址的时候,段寄存器+偏移地址经过转换计算以后得到的地址是null物理地址null,也就是在物理内存中的实际地址。而保护模式下,段选择器+偏移地址转换后的地址被称为null线性地址null而不是null物理地址null。那么,线性地址就是物理地址吗?
答案可能是null是null,也可能是null不是null,这取决于80386的内存分页机制是否被使用。
在单任务的DOS系统中,一个应用程序可以使用所有的空闲内存。程序退出后,操作系统回收所有的碎片内存并且合并成一个大块内存继续供下一个程序使用。内存合并过程中的一个极端情况是当系统中有多个TSR程序时,早装入内存的TSR卸载后,后装入的TSR
会留在内存的中间部位,把空闲内存隔成两个区域。这时应用程序使用的最大内存块只能是这两块内存中较大的一块,无法将它们合并使用。
对于一个多任务的操作系统,内存的碎片化是不能容忍的。否则,经过一段时间后,即使空闲内存的总和很大,也可能出现任何一片内存都小到无法装入执行程序的地步。所以多任务操作系统中碎片内存的合并是个很重要的问题。
80386处理器的分页机制可以很好地解决这个问题。80386处理器把4 KB大小的一块内存当做一null页null内存,每页物理内存可以根据null页目录null和null页表null,随意映射到不同的线性地址上。这样,就可以将物理地址不连续的内存的映射连到一起,在线性地址上视为连续。
在80386处理器中,除了和CR3寄存器(指定当前页目录的地址)相关的指令使用的是物理
地址外,其他所有指令都是用线性地址寻址的。
是否启用内存分页机制是由80386处理器新增的CR0寄存器中的位31(PG位)决定的。
如果PG=0,则分页机制不启用,这时所有指令寻址的地址(线性地址)就是系统中实际的物理地址;当PG=1的时候,80386处理器进入内存分页管理模式,所有的线性地址要经过页表的映射才得到最后的物理地址。
以图1.4为例,一个xxxx:yyyyyyyy格式的虚拟地址,经过图1.3所示的段地址转换步骤后得到32位的线性地址zzzzzzzz(步骤①)。当禁用分页机制时,线性地址就是物理地址,
处理器直接从物理内存存取数据(步骤②);当启用分页机制时,得到线性地址的方法还是一样(步骤○1'),但是还要根据页目录和页表指定的映射关系把地址映射到物理内存的真正位置上(步骤○3')。然后,CPU以映射后的物理地址在物理内存中存取数据。这个过程对于指令来说是透明的。
虚拟地址
xxxx:yyyyyyyy
ffffffff
zzzzzzzz
00000000
物理内存地址
ffffffff ffffffff
00000000
物理内存地址
00000000
线性地址
分页允许
32位线性地址
页地址转换
映射关系由
页目录和页表指定
分页禁止
段地址转换
参考图1.3
zzzzzzzz
0 31
4k
4k
4k
4k
4k zzzzzzzz
图1.4 80386的内存地址转换
内存分页管理只能在保护模式下才可以实现,实模式不支持分页机制。但不管在哪种模式下,所有寻址指令使用的都是线性地址,程序不用关心数据最后究竟存放在物理内存的哪个地方。
页表规定的不仅是地址的映射,同时还规定了页的访问属性,如是否可写、可读和可执行等。比如把代码所在的内存页设置为可读与可执行,那么权限不够的代码向它写数据就会引发保护异常。利用这个机制可以在硬件层次上支持虚拟内存的实现。
如图1.5所示,页表可以指定一个页面并不真正映射到物理内存中。这样,访问这个页的指令会引发页异常错误。这时,处理器会自动转移到页异常处理程序中去。操作系统可以在异常处理程序中将硬盘上的虚拟内存读到内存中并修改页表重新映射,然后重新执行引发异常的指令。这样指令可以正常执行下去。
数据被访问
将数据从虚拟内存
文件中读出并重新映射到空闲RAM
RAM中有空闲
的页面吗?
在RAM中
找一个不常
访问的页面
在虚拟内存
文件中吗? 错误
该页面
数据是否无效?
把该页面保
存到虚拟内
存文件中去
物理内存中吗?
数据在
访问数据 当页表中的
数据位表示页面没有映
射到物理内存中时,指令会引发
异常,于是操作系统可以在异常处理
中进行以下操作,引发异常的指令不会意识到这个过程
Yes
No
No
No No
Yes
Yes Yes
图1.5 虚拟内存的实现
4,Windows的内存安排
Windows系统一般在硬盘上建立大小为物理内存两倍左右的交换文件(文件名在
Windows 9x下为Win386.swp,Windows NT下为PageFile.sys)用做虚拟内存。利用80386
处理器的内存分页机制,交换文件在寻址上可以很方便地作为物理内存使用。只需在真正访问到的时候将硬盘文件的内容读入物理内存,然后重新将线性地址映射到这块物理内存就可以了。同样道理,被执行的可执行文件也不必真正装入内存,只要在页表中建立映射关系,
以后到真正访问到的时候再调入物理内存。
如果把虚拟内存暂时先视为物理内存的一部分,从物理内存中的层次看,Windows操作系统和DOS一样,也是所有的内容共享内存,如操作系统使用的代码和数据(GDT,LDT
与页表等),当前执行中的所有程序的代码和数据以及这些程序调用的DLL的代码和数据等,
如图1.6的左上角所示。
但是从应用程序代码的层次看,也就是说从分页映射后线性地址的层次看,内存的安排却不是这个样子。因为Windows是一个分时的多任务操作系统,CPU时间被分成一个个的时间片后分配给不同程序轮流使用,在一个程序的时间片中,和这个程序执行无关的东西(如其他程序的代码和数据)并不需要映射到线性地址中去。
物理内存空间+虚拟内存
操作系统的
代码、数据
系统
DLL
用户
DLL
进程A
进程B
进程C
Windows 98下
一个进程的
线性地址安排
ffffffff
8 0000000
1 0000000
c 0000000
0 0400000
00001000
0 0000000
操作系统代码
系统DLL
内存映射文件
用户DLL
进程堆栈
内存分配堆
进程代码
和数据
DOS/Win16
兼容区域
空指针区
时间片n 时间片2 时间片1 时间
用户
DLL
系统
DLL
操作
系统
操作
系统
操作
系统
系统
DLL
系统
DLL
用户
DLL
用户
DLL
进程C
代码
数据
进程B
代码
数据
进程A
代码
数据
线性地址
空间
0 0000000
ffffffff
内存分页
图1.6 Windows的内存安排
如图1.6所示,Windows操作系统通过切换不同的页表内容让线性地址在不同的时间片中映射不同的内容。图中的右边是Windows 98操作系统在单个时间片中线性地址的安排
(Windows NT稍微有些不同)。在物理内存中,操作系统和系统DLL的代码需要供每个应用程序调用,所以在所有的时间片中都必须被映射;用户程序只在自己所属的时间片内被映射;而用户DLL则有选择地被映射。假设程序A和程序C都要用到xxx.dll,那么物理内存中xxx.dll的代码在图中的时间片1和n中被映射,其他的时间片就不需要映射,当然,物理内存中只需要一份xxx.dll的代码。
由此可以引出Win32编程中几个很重要的概念,
每个应用程序都有自己的 4 GB 的寻址空间。该空间可存放操作系统、系统 DLL 和用户DLL的代码,它们之中有各种函数供应用程序调用。再除去其他的一些空间,余下的是应用程序的代码、数据和可以分配的地址空间。
不同应用程序的线性地址空间是隔离的。虽然它们在物理内存中同时存在,但在某个程序所属的时间片中,其他应用程序的代码和数据没有被映射到可寻址的线性地址中,
所以是不可访问的。从编程的角度看,程序可以使用4 GB的寻址空间,而且这个空间是null私有null的。
DLL 程序没有自己null私有null的空间。它们总是被映射到其他应用程序的地址空间中,
当做其他应用程序的一部分运行。原因很简单,如果它不和其他程序同属一个地址空间,应用程序该如何调用它呢?
5,从Win32汇编的角度看内存寻址
对初学者来说,DOS下的分段寻址方式就已经令人一头雾水了,80386保护模式的内存管理就更麻烦。的确,如果在Win32汇编中访问内存之前要先在描述符表中构造正确的描述符,然后再构造页表把物理内存映射到要访问的线性地址的话,那就简直是一场噩梦,
有90%的汇编程序员会因此改行去卖茶叶蛋!
但实际上这并没有发生,因为Win32汇编中的内存访问远比DOS下的分段寻址方式简单,这是为什么呢?
因为Windows是一个多任务的操作系统,最首要的宗旨就是null稳定压倒一切null。但如果把描述符表以及页表等内容交给用户程序是很不安全的,不用说全局描述符表,就是为每个程序建立的局部描述符表也不应该让用户程序改写,否则用户可以通过构造自己的描述符来访问操作系统不希望用户访问的东西。任何权限上开放引发的安全问题都是很严重的,如
Windows 9x中的中断描述符表是可写的,CIH病毒可利用它将自己的权限提高到优先级0;
而Windows NT下的中断描述符表是不可写的,CIH病毒在Windows NT下就无法进驻内存。
正因为如此,Windows操作系统干脆为用户程序null安排好了一切null。具体表现在为用户程序的代码段、数据段和堆栈段全部预定义好了段描述符。这些段的起始地址为0,限长为
ffffffff,所以用它们可以直接寻址全部的4 GB地址空间。程序开始执行的时候,CS,DS,
ES和SS都已经指向了正确的描述符,在整个程序的生命周期内,程序员不必改动这些段寄存器,也不必关心它们的值究竟是多少(实际上,想改也改不了)。
所以对Win32汇编程序来说,整个源程序中竟然可以不用出现段寄存器的身影。这在
DOS汇编编程中是不可想像的。回顾本节开头提出的问题,答案是:并不是Win32汇编源代码用不到段寄存器,而是用户在使用中不必去关心段寄存器!
1.3.3 Windows的特权保护
Windows的特权保护和处理器硬件的支持是分不开的。优先级的划分、指令的权限检查和超出权限访问的异常处理等是构成特权保护的基础。这一节将简单介绍这些课题,读者可以考虑一下初学Win32汇编时遇到的疑问,
1,Win32 汇编中为什么找不到中断指令的使用?
2,Windows 错误的蓝屏幕是从哪里来的?
1,80386的中断和异常
中断指当程序执行过程中有更重要的事情需要实时处理时(如串口中有数据到达,不及时处理数据会丢失,串行控制器就提交一个中断信号给处理器要求处理),硬件通过中断控制器通知处理器。处理器暂时挂起当前运行的程序,转移到中断处理程序中;当中断处理程序处理完毕后,通过iret指令回到原先被打断的程序中继续执行。
异常指指令执行中发生不可忽略的错误时(如遇到无效的指令编码,除法指令除零等),
处理器用和中断处理相同的操作方法挂起当前运行的程序转移到异常处理程序中。异常处理程序决定在修正错误后是否回到原来的地方继续执行。
更为DOS汇编程序员熟悉的null中断null指的是用int n指令直接转移到中断向量n指定的中断处理程序中执行。严格地讲,int n指令应该算null自陷null而不是null中断null。因为这时并不
是程序被急需解决的事情打断。而是自己要求停止执行并转移到中断处理程序中去。
不管中断、异常还是自陷,虽然它们产生的原因不同,但处理过程是类似的,都通过中断向量表里存放的入口地址转移到服务程序,都由CPU自动在堆栈中保护断点地址,最后也都可以用iret指令返回指令被中断的地方。
先回顾一下8086或80386实模式下中断和异常的处理过程。如图1.7所示,实模式下的中断和异常服务程序地址存放在中断向量表中。中断向量表位于物理内存00000h开始的400h
字节中,共支持100h个中断向量;每个中断向量是一个xxxx:yyyy格式的地址,占用4字节。
当发生n号异常或n号中断,或者执行到int n指令的时候,CPU首先到内存nnull4的地方取出服务程序的地址aaaa:bbbb(图示步骤①);然后将标志寄存器、中断时的CS和IP压入堆栈,接着转移到aaaa:bbbb处执行(步骤②);在服务程序最后遇到iret的时候,CPU从堆栈中恢复标志寄存器,然后取出CS和IP并返回。
中断向量表
中断服务
程序入口
物理内存
物理内存 地址
中断向量 ff
中断向量 n
中断向量 02
中断向量 01
中断向量 00
int n指令
n 号异常
n 号中断
xxxx:yyyy
xxxx:yyyy
xxxx:yyyy
xxxx:yyyy
aaaa:bbbb
aaaa:bbbb
00400h
n × 4
0000ch
00008h
00004h
00000h
1
2
图1.7 实模式下中断和异常的处理
在保护模式下,中断或异常处理往往从用户代码切换到操作系统代码中执行。由于保护模式下的代码有优先级之分,因此出现了从优先级低的应用程序转移到优先级高的系统代码中的问题,如果优先级低的代码能够任意调用优先级高的代码,就相当于拥有了高优先级代码的权限。为了使高优先级的代码能够安全地被低优先级的代码调用,保护模式下增加了null门null
的概念。null门null指向某个优先级高的程序所规定的入口点,所有优先级低的程序调用优先级高的程序只能通过门重定向,进入门所规定的入口点。这样可以避免低级别的程序代码从任意位置进入优先级高的程序的问题。保护模式下的中断和异常等服务程序也要从null门null进入,
80386的门分为中断门、自陷门和任务门几种。
在保护模式下要表示一个中断或异常服务程序的信息需要用8个字节,包括门的种类以及xxxx:yyyyyyyy格式的入口地址等。这组信息叫做null中断描述符null。这样,中断向量表就无法采用和实模式下同样的4字节一组的格式。保护模式下把所有的中断描述符放在一起组成
null中断描述符表nullIDT(Interrupt Descriptor Table)。IDT不再放在固定的地址00000h处,而是采用可编程设置的方式,支持的中断数量也可以设置。为此80386处理器引入了一个新的
48位寄存器IDTR。IDTR的高32位指定了IDT在内存中的基址(线性地址),低16位指定了IDT的长度,相当于指定了可以支持的中断数量。
如图1.8所示,保护模式下发生异常或中断时,处理器先根据IDTR寄存器得到中断描述符的地址,然后取出n号中断/异常的门描述符,再从描述符中得到中断服务程序的地址
xxxx:yyyyyyyy,经过段地址转换后得到服务程序的32位线性地址并转移后执行。
47 15 0
IDTR 基址 限长
中断描述符表
中断或
异常
服务程序 n号异常
n号中断
int n指令
基址 线性地址
描述符N
描述符n
描述符2
描述符1
描述符0
nnull8h
Nnull8h
0010h
0008h
0000h
内存线性地址
xxxx:yyyyyyyy
图1.3
所示段地址
转换
图1.8 保护模式下的中断和异常处理
由于保护模式下用中断门可以从低优先级的代码调用高优先级的代码,所以不能让用户程序写中断描述符表,否则会引发安全问题(又想到了CIH病毒)。这样就如关了窗子挡住苍蝇,也挡住了微风,用户的系统扩展程序也就不能像在DOS中一样再用中断服务程序的方式提供服务了。因为用户程序根本没有权限将中断地址指到自己的代码中来。
在Windows中,操作系统使用动态链接库来代替中断服务程序提供系统功能,所以
Win32汇编中int指令也就失去了存在的意义。这就是在Win32汇编源代码中看不到int指令的原因。其实那些调用API的指令原本是用int指令实现的。
2,80386的保护机制
80286之前的处理器只支持单任务,操作系统并没有什么安全性可言,计算机的全部资源包括操作系统的内部资源都可以任凭程序员调用。但对于多任务的操作系统,某个捣乱的程序为所欲为令使所有程序都无法运行。所以80286及以上的处理器引入了优先级的概念。
80386处理器共设置4个优先级(0~3)。0级是最高级(特权级);3级是最低级(用户级);
1级和2级介于它们之间。特权级代码一般是操作系统的代码,可以访问全部系统资源;其他级别的代码一般是用户程序,可以访问的资源受到限制。
80386采用保护机制主要为了检查和防止低级别代码的越权操作,如访问不该访问的数据、端口以及调用高优先级的代码等。保护机制主要由下列几方面组成,
● 段的类型检查null椂null的类型是由段描述符指定的,主要属性有是否可执行,是否可读和是否可写等。而CS,DS和SS等段选择器是否能装入某种类型的段描述符是有限制的。
如不可执行的段不能装入CS;不可读的段不能装入DS与ES等数据段寄存器;不可写的段不能装入SS等。如果段类型检查通不过,则处理器会产生一般性保护异常或堆栈异常。
● 页的类型检查棗除了可以在段级别上指定整个段是否可读写外,在页表中也可以为每个页指定是否可写。对于特权级下的执行代码,所有的页都是可写的。但对于1,2和3级的代码,还要根据页表中的R/W项决定是否可写,企图对只读的页进行写操作会产生页异常。
● 访问数据时的级别检查优先级低的代码不能访问优先级高的数据段。80386 的段描述符中有一个DPL域(描述符优先级),表示这个段可以被访问的最低优先级。而段选择器中含有RPL域(请求优先级),表示当前执行代码的优先级。只有DPL在数值上大于或等于RPL值的时候,该段才是可以访问的,否则会产生一般性保护异常。
● 控制转移的检查nullnull在处理器中,有很多指令可以实现控制转移,如 jmp,call,ret,
int 和 iret 等指令。但优先级低的代码不能随意转移到优先级高的代码中,所以遇到这些指令的时候,处理器要检查转移的目的位置是否合法。
● 指令集的检查nullnull有两类指令可以影响保护机制。第一类是改变 GDT,LDT,IDT 以及控制寄存器等关键寄存器的指令,称为特权指令;第二类是操作 I/O 端口的指令以及 cli
和 sti 等改变中断允许的指令,称为敏感指令。试想一下,如果用户级程序可以用 sti 禁止一切中断(包括时钟中断),那么整个系统就无法正常运行,所以这些指令的运行要受到限制。
特权指令只能在优先级 0 上才能运行,而敏感指令取决于 eflags 寄存器中的 IOPL 位。只有
IOPL 位表示的优先级高于等于当前代码段的优先级时,指令才能执行。
* I/O 操作的保护nullnullI/O 地址也是受保护的对象。因为通过 I/O 操作可以绕过系统对很多硬件进行控制。80386 可以单独为 I/O 空间提供保护,每个任务有个 TSS(任务状态段)
来记录任务切换的信息。TSS 中有个 I/O 允许位图,用来表示对应的 I/O 端口是否可以操作。
某个 I/O 地址在位图中的对应数据位为 0 则表示可以操作;如果为 1 则还要看 eflags 中的 IOPL
位,这时只有 IOPL 位表示的优先级高于等于当前代码段的优先级,才允许访问该 I/O 端口。
3,W indows 的保护机制
在 Windows 下,操作系统运行于 0 级,应用程序运行于 3 级。因为 Alpha 计算机只支持两个优先级,为了便于将应用程序移植到 Alpha 计算机上,Windows 操作系统不使用 1 和 2
级这两个优先级。
Windows 操作系统充分利用 80386 的保护机制,所有和操作系统密切相关的东西都是受保护的。运行于优先级 3 上的用户程序有很多限制,只有在写 VxD 等驱动程序的时候才可以使用全部资源。在 Win32 汇编编程中要注意避免以下的越权操作(当然写驱动程序不在此列),
1,显而易见,所有的特权指令都是不可执行的,如 lgdt,lldt,lidt 指令和对 CRx 与 TRx
等寄存器赋值。但是,读取重要寄存器的指令是可以执行的,如 sgdt,sldt 和 sidt 等。
2,Windows 在页表中把代码段和数据段中的内存页赋予不同的属性。代码段是不可写的,
数据段中也只有变量部分的页面是可写的。所以虽然可以寻址所有的 4 GB 空间,但访问超出权限规定以外的东西还是会引发保护异常。
3,在 Windows 98 中,系统硬件用的 I/O 端口是受保护的,但其余的则可以操作。如果用户在机器中插了一块自己的卡,用的是 300h 等系统未定义的端口,那么在应用程序中就可以直接操作,但要操作 3f8h(串口)和 1f0h(硬盘端口)等系统已定义的端口就不行了。在
Windows NT 中,任何的端口操作都是不允许的。
如果违反了 Windows 规定的null保护条例null,那么会引发保护异常,处理器会毫不犹豫地把控制权转移到对应的异常处理程序中去。Windows 会在处理程序中用一个很酷的null非法操作null对话框把用户的程序判死刑,没有一点回旋的余地!在 Windows 9x 中,系统有时会用一个蓝屏幕来通知用户程序试图访问不存在的内存页。
如果程序调用的 DLL 中有错,那么错误还是会算在应用程序头上,因为 DLL 的地址间是被映射到应用程序的空间中去的。Windows 9x 本身是 32 位和 16 位混合的操作系统,为了兼容DOS和Win16程序,很多的保护措施做起来力不从心。所以系统内部反而常常出现越权操作,以至于蓝屏幕不断,这些就不是用户应用程序自己的问题了。