在一个月前我打算学Win32汇编,发现汇编的基础不是太好,便花了近一个月的时间把王爽老师的《汇编语言》看了一遍。现在再来看Win32汇编,比一个月前轻松了不少。真是本看书,对于我个人来说。
接下来,我将继续Win32汇编的学习。坚持、坚持!
Let's go!
Background
Windows环境下32位汇编语言是一种全新的编程语言。它使用与C++语言相同的API接口,不仅可以用来开发出大型的软件,而且是了解操作系统运行细节的最佳方式。
Win32指的是32位的Windows系列操作系统。
1990年5月份微软推出了Windows 3.0,可以支持Intel 80286/386/486微处理器的保护模式,并可以访问达16MB的内存。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,主要针对网络和服务器市场。NT代表新技术(New Technology)。NT 3.11是Windows系列中使用32位编程模式的第一个版本。它充分利用80386及以上处理器的平面地址空间和保护模式等新技术。
1995年8月,Microsoft推出新一代操作系统Windows 95。Windows 95实现了很友好的用户界面,支持即插即用功能,支持主流多媒体设备和DirectX编程接口,成为Microsoft发展史上的一个里程碑,也是操作系统发展史上的一个里程碑。从此,Windows 9x便取代了Windows 3.x和MS-DOS操作系统,成为个人计算机平台的主流操作系统。
1998年Microsoft又发布了使用更方便的Windows 98。
在操作系统的分类上,Microsoft根据家庭个人用户和商业办公用户的不同需求,分别提供Windows 9x和Windows NT系列,Windows 9x注重用户界面及其他易用性特征,而NT系列则在纯32位内核的稳定性和可靠性等企业级特征上下功夫。
2000年,微软发布采用纯32位内核并照顾了家庭消费类应用软件的Windows NT 5.0,即Windows 2000。至此Micosoft的两个系列操作系统(9x和NT)终于开始统一。
为了利用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也随处可见。
Windows的特色
对于使用者来说:
1)图形用户界面;(GUI,Graphic User Interface) Windows最重要的特色。
2)一致的用户界面; 便于使用。
3)多任务。 非常重要的特色。用户可以同时运行多个程序,可以在不同的程序之间传送数据。
对于程序员来说,更关心隐藏在底下的东西:
1)大量的函数调用;Win32支持上千种函数的调用,几乎涉及所有的方面,程序员可以把更多的时间放在程序的逻辑结构和用户界面上。
2)和设备的无关性;Win32程序并不直接访问屏幕、打印机和键盘等硬件设备,Windows虚拟了所有的硬件。只要有硬件的设备驱动程序,这个硬件就可以使用,应用程序并不需要关心硬件的具体型号。与DOS编程中需要针对不同的显示卡和打印机等编写很多的驱动程序来比,这个特性对程序员的帮助是巨大的。
3)内存管理。由于内存分页和虚拟内存的使用,每个程序都可以使用4GB的地址空间,DOS编程时必须考虑的640KB的内存问题已经成为历史。
必须了解的东西
80x86处理器的工作模式
80386处理器有3种工作模式:实模式、保护模式和虚拟86模式。
实模式和虚拟86模式是为了和8086处理器兼容而设置的。在实模式下,80386处理器就相当于一个快速的8086处理器。
保护模式是80386处理器的主要工作模式。在此方式下,80386可以寻址4GB的地址空间,同时,保护模式提供了80386先进的多任务、内存分页管理和优先级保护等机制。
为了在保护模式下继续提供和8086处理器的兼容,80386又设计了一种虚拟86模式,以便可以在保护模式的多任务条件下,有的任务运行32位程序,有的任务运行MS-DOS程序。
在虚拟86模式下,同样支持任务切换、内存分页管理和优先级,但内存的寻地址方式和8086相同,也是可以寻址1MB的空间。
实模式是80386处理器工作的基础,这时80386当做一个快速的8086处理器工作。在实模式下可以通过指令切换到保护模式,也可以从保护模式退回实模式。
虚拟86模式则以保护模式为基础,在保护模式和虚拟86模式之间可以互相切换,但不能从实模式直接进入虚拟86模式或从虚拟86模式直接退到实模式。
实模式
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位寄存器进行编程可以使计算程序更加简捷,加快了执行速度。其次,80386中增加的两个辅助段寄存器FS和GS在实模式下也可以使用,这样,同时可以访问的段达到了6个而不必考虑重新装入的问题;最后,很多80386的新增指令也使一些原来不很方便的操作得以简化,如80386中可以使用下述指令进行数组访问:
mov cx,[eax + ebx * 2 + 数组基地址]
这相当于把数组下标为eax和ebx的项目放入cx中;ebx*2中的2可以是1,2,4或8,这样就可以支持8位到64位的数组。
另外,pushad和popad指令可以一次把所有8个通用寄存器的值压入或从堆栈中弹出,比起用下面的指令分别将8个寄存器入栈要快了很多:
push eax
push ebx
…
pop ebx
pop eax
当然,使用了这些新指令的程序是无法拿回到8086处理器上去执行的,因为这些指令的编码在8086处理器上是未定义的。
保护模式
当80386工作在保护模式下的时候,它的所有功能都是可用的。这时80386所有的32根地址线都可供寻址,物理寻地址空间高达4GB。在保护模式下,支持内存分页机制,提供了对虚拟内存的良好支持。虽然与8086可寻址的1MB物理地址空间相比,80386可寻地址的物理地址空间可谓很大,但实际的微机系统不可能安装如此大的物理内存。所以,为了运行大型程序和真正实现多任务,虚拟内存是一种必需的技术。
保护模式下80386支持多任务,可以依靠硬件仅在一条指令中实现任务切换。任务环境的保护工作是由处理器自动完成的。在保护模式下,80386处理器还支持优先级机制,不同的程序可以运行在不同的优先级上。优先级一共分0~3 四个级别,操作系统运行在最高的优先级0上,应用程序则运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好地隔离各个任务。从实模式切换到保护模式是通过修改控制寄存器CR0的控制位PE(位0)来实现的。在这之前还需要建立保护模式必需的一些数据表,如全局描述符表GDT和中断描述符表IDT等。
DOS操作系统运行于实模式下,而Windows操作系统运行于保护模式下。
虚拟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当作基址再配合偏移地址形成线性地址,寻址空间为1MB。但显然多个虚拟86任务不能同时使用同一位置的1MB地址空间,否则会引起冲突。操作系统利用分页机制将不同虚拟86任务的地址空间映射到不同的物理地址上去,这样每个虚拟86任务看起来都认为自己在使用0~1MB的地址空间。
8086代码中有相当一部分指令在保护模式下属于特权指令,如屏幕中断的cli和中断返回指令iret等。这些指令在8086程序中是合法的。如果不让这些指令执行,8086代码就无法工作。为了解决这个问题,虚拟86管理程序采用模拟的方法完成这些指令。这些特权指令执行的时候引起了保护异常。虚拟86管理程序在异常处理程序中检查产生异常的指令,如果是中断指令,则从虚拟86任务的中断向量表中取出中断处理程序的入口地址,并将控制转移过去;如果是危及操作系统的指令,如cli等,则简单地忽略这些指令,在异常处理程序返回的时候直接返回到下一条指令。通过这些措施,8086程序既可以正常地运行下去,在执行这些指令的时候又觉察不到已经被虚拟86管理程序做了手脚。MS-DOS应用程序在Windows操作系统中就是这样工作的。
Windows的内存管理
Win32汇编中,每个程序都可以用4GB的内存吗?
Win32汇编源代码中为什么看不到CS、DS、ES和SS等段寄存器的使用?
DOS操作系统的内存安排
Win32编程相对于DOS编程最大的区别之一就是内存的使用。
00000h
|
中断向量表
|
00400h
|
BIOS数据区
|
00500h
|
DOS数据区
|
|
系统程序
(DOS的驻留部分、驱动程序等)
|
|
可用空间
|
A0000h
|
图形模式视频缓冲区
|
B0000h
|
单色字符模式视频缓冲区
|
B8000h
|
彩色字符模式视频缓冲区
|
C0000h
|
VGA BIOS地址
|
C8000h
|
ROM扩展、系统BIOS地址
|
FFFFFh
|
64KB高端内存
|
在实模式下,一个内存寻址方式是,一个完整的地址由段地址和偏移地址两部分组成。段地址放在16位的段寄存器中,然后在指令中用16位的偏移地址寻址。处理器换算时先将段地址乘以10h(即16),得到段在物理内存中的起始地址;然后 加上16位的偏移地址得到实际的物理地址。
当80386处理器工作在保护模式和虚拟8086模式的时候,可以使用全部32根地址线访问内存4GB大的内存。段地址加偏移地址的计算方法显然无法覆盖这么大的范围。但计算一下就可以发现,实际上和8086同样的限制已经不复存在,因为80386所有的通用寄存器都是32位的,2的32次方相当于4G,所以用任何一个通用寄存器来间接寻址,不必分段就已经可以访问到所有的内存地址。
这是不是说,在保护模式下,段寄存器就不再有用了呢?答案是否定的。实际上段寄存器更有用了,虽然在寻址上不再有分段的限制问题,但在保护模式下,一个地址空间是否可以被写入,可以被多少优先级的代码写入,是不是允许执行等涉及保护的问题就出来了。要解决这些问题,必须对一个地址空间定义一些安全上的属性。段寄存器这时就派上了用途。但是涉及属性和保护模式下段的其他参数,要表示的信息太多了,要用64位长的数据才能表示。我们把这64位的属性数据叫做段描述符(Segment Desciptor)。
80386的段寄存是16位的,无法放下保护模式下64位的段描述符。解决办法是把所有段的段描述符顺序放在内存中的指定位置,组成一个段描述符表(Descriptor Table);而段寄存器中的16位用来做索引信息,指定这个段的属性用段描述符表中的第几个描述符来表示。这时,段寄存器中的信息不再是段地址了,而是段选择器(Segment Selector)。可以通过它在段描述符表中,选择一个项目以得到段的全部信息。
既然这样,段描述符表放在哪里呢?80386中引入了两个新的寄存器来管理段描述符表。一个是48位的全局描述符表寄存器GDTR(Global Descriptor Table Register),一个是16位的局部描述符表寄存器LDTR(Local Descriptor Table Register)。那么,为什么有两个描述符表寄存器呢?
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中。
80386的内存分页机制
在实模式下寻址的时候,段寄存器+偏移地址经过转换计算以后得到的地址是物理地址,也就是在物理内存中的实际地址。
而保护模式下,段选择器+偏移地址转换后的地址被称为线性地址,而不是物理地址。
线性地址是物理地址吗?
可能是,也可能不是,这取决于80386的内存分页机制是否被使用。
在单任务的DOS系统中,一个应用程序可以使用所有的空闲内存。程序退出后,操作系统回收所有的碎片内存并且合并成一个大块内存继续供下一个程序使用。内存合并过程中的一个极端情况汇报系统中有多个TSR卸载后,后装入的TSR会留存内存的中间部位,把空闲内存隔成两个区域。这时应用程序使用的最大内存块只能是这两块内存中较大的一块,无法将它们合并使用。
对于一个多任务的操作系统,内存的碎片化是不能容忍的。否则,经过一段时间后,即使空闲内存的总和很大,也可能出现任何一处内存都小到无法装入执行程序的地步。所以多任务操作系统中碎片内存的合并是个很重要的问题。
80386处理器的分页机制可以很好地解决这个问题。80386处理器把4KB大小的一块内存当做一页内存,每页物理内存可以根据页目录和页表,随意映射到不同的线性地址上。这样,就可以将物理地址不连续的内存的映射连到一起,在线性地址上视为连续。在80386处理器中,除了和CR3寄存器(指定当前页目录的地址)相关的指令使用的是物理地址外,其他所有指令都是用线性地址寻址的。
是否启用内存分页机制是由80386处理器新增的CR0寄存器中的位31(PG位)决定的。如果PG=0,则分页机制不启用,这时所有指令寻址的地址(线性地址)就是系统中实际的物理地址;当PG=1的时候,80386处理器进入内存分页管理模式,所有的线性地址要经过页表的映射才得到最后的物理地址。
内存分页管理只能在保护模式下才可以实现,实模式不支持分页机制。但不管在哪种模式下,所有寻址指令使用的都是线性地址,程序不用关心数据最后究竟存放在物理内存的哪个地方。
页表规定的不仅是地址的映射,同时还规定了页的访问属性,如是否可写、可读和可执行等。比如把代码所在的内存页设置为可读与可执行,那么权限不够的代码向它写数据就会引发保护异常。利用这个机制可以在硬件层次上支持虚拟内存的实现。
页表可以指定一个页面并不真正映射到物理内存中,这样,访问这个页的指令会引发页异常错误。这时,处理器会自动转移到页异常处理程序中去,操作系统可以在异常处理程序中将硬盘上的虚拟内存读到内存中并修改页表重新映射,然后重新执行引发异常的指令。这样指令可以正常执行下去。
Windows的内存安排
Windows系统一般在硬盘上建立大小为物理内存两倍左右的交换文件(文件名在Windows 9x下为Win386.swp,Windows NT下为PageFile.sys)用做虚拟内存。
利用80386处理器的内存分页机制,交换文件在寻地址上可以很方便地作为物理内存使用。只需在真正访问到的时候将硬盘文件的内容读入物理内存,然后重新将线性地址映射到这块物理内存就可以了。同样道理,被执行的可执行文件也不必真正装入内存,只要在页表中建立映射关系,以后到真正访问到的时候再调入物理内存。
如果把虚拟内存暂时先视为物理内存的一部分,从物理内存中的层次看,Windows操作系统和DOS一样,也是所有的内容共享内存,如操作系统使用的代码和数据(GDT,LDT与页表等),当前执行中的所有程序的代码和数据以及这些程序调用的DLL的代码和数据等。
但是从应用程序代码的层次看,也就是说从分页映射后线性地址层次看,内存的安排却不是这个样子。因为Windows是一个分时的多任务操作系统,CPU时间被分成一个个的时间片后分配给不同程序轮流使用,在一个程序的时间片中,和这个程序执行无关的东西(如其他程序的代码和数据)并不需要映射到线地址中去。
Windows操作系统通过切换不同的页表内容让线性地址在不同的时间片中映射不同的内容。在物理内存中,操作系统和系统DLL的代码需要供每个应用程序调用,所以在所有的时间片中都必须被映射;用户程序只在自己所属的时间片内被映射;而用户DLL则有选择地被映射。
Win32编程中几个很重要的概念:
每个应用程序都有自己的4GB的寻址空间。该空间可存放操作系统、系统DLL和用户DLL的代码,它们之中有各种函数供应用程序调用。再除去其他的一些空间,余下的是应用程序的代码、数据和可以分配的地址空间。
不同应用程序的线性地址空间是隔离的。虽然它们在物理内存中同时存在,但在某个程序所属的时间片中,其他应用程序程序的代码和数据没有被映射到可寻地址的线性地址中,所以是不可访问的。从编程的角度看,程序可以使用4GB的寻址空间,而这个空间是私有的。
DLL程序没有自己的私有的空间。它们总是被映射到其他应用程序的地址空间中,当做和其他应用程序的一部分运行。原因很简单,如果它不和其他程序同属一个地址空间,应用程序该如何调用它呢?
从Win32汇编的角度看内存寻址
DOS下的分段寻地址方式令人一头雾水,80386保护模式的内存管理就更复杂。
Windows是一个多任务操作系统,最首要的宗旨就是稳定压倒一切,如果描述符表以及页表等内容交给用户程序是很不安全的,不用说全局描述符表,就是为每个程序建立的局部描述符表也不应该让用户程序改写,否则用户可以通过构造自己的描述符来访问操作系统不希望用户访问的东西。任何权限上开放引发的安全问题都是很严重的。
病毒程序就是利用这些系统漏洞,提升自身的权限,做一些破坏动作的。
所以,Windows操作系统干脆为用户程序安排好了一切。具体表现在为用户程序的代码段、数据段和堆栈段全部预定义好了段描述符。这些段的起始地址为0,限长为ffffffff,所以用它们可以直接寻址全部的4GB地址空间。程序开始执行的时候,CS,DS,ES和SS都已经指向了正确的描述符,在整个程序的生命周期内,程序员不必改动这些段寄存器,也不必关心它们的值究竟是多少(实际上,想改也改不了)。
所以对Win32汇编程序来说,整个源程序中竟然可以不用出现段寄存器的身影。这在DOS汇编编程中是不可想象的。
并不是Win32汇编源代码用不到段寄存器,而是用户在使用中不必去关心段寄存器。
Windows的特权保护
Windows的特权保护和处理器硬件的支持是分不开的。
优先级的划分、指令的权限检查和超出权限访问的异常处理等是构成特权保护的基础。
1、Win32汇编中为什么找不到中断指令的使用?
2、Windows错误的蓝屏屏幕是从哪里来的?
80386的中断和异常
中断指当程序执行过程中有更重要的事情需要实时处理时(如串口中有数据到达,不及时处理数据会丢失,串行控制器就提交一个中断信号给处理器要求处理),硬件通过中断控制器通知处理器。处理器暂时挂起当前运行的程序,转移到中断处理程序中;当中断处理程序处理完毕后,通过iret指令回到原先被打断的程序中继续执行。
异常指指令执行中发生不可忽略的错误时(如遇到无效的指令编码,除法指令除零等),处理器用和中断处理相同的操作方法挂起当前运行的程序转移到异常处理程序中。异常处理程序决定在修正错误后是否回到原来的地方继续执行。
更为DOS汇编程序员熟悉的中断,指的是用int n指令直接转移到中断向量n指定的中断处理程序中执行。严格地讲,int n指令应该算自陷而不是中断。因为这时并不是程序被急需解决的事情打断。而是自己要求停止执行并转移到中断处理程序中去。
不管中断、异常还是自陷,虽然它们产生的原因不同,但处理过程是类似的,都通过中断向量表里存放的入口地址转移到服务程序,都由CPU自动在堆栈中保护断点地址,最后也都可以用iret指令返回指令被中断的地方。
8086或80386实模式下中断和异常的处理过程。
中断和异常服务程序地址存放在中断向量表中。中断向量表位于物理内存00000h开始的400h字节中,共支持100h个中断向量;每个中断向量是一个xxxx:yyyy格式的地址,占用4字节。当发生n号异常或n号中断,或者执行到int n指令的时候,CPU首先到内存n 4的地方取出服务程序的地址aaaa:bbbb;然后将标志寄存器、中断时的CS和IP压入堆栈,接着转移到aaaa:bbbb处执行;在服务程序最后遇到iret的时候,CPU从堆栈中标志寄存器,然后取出CS和IP并返回。
在保护模式下,中断或异常处理往往从用户代码切换到操作系统代码中执行。由于保护模式下的代码有优先级之分,因此出现了从优先级低的应用程序转移到优先级高的系统代码中的问题,如果优先级低的代码能够任意调用优先级高的代码,就相当于拥有了高优先级代码的权限。为了使高优先级的代码能够安全地被低优先级的代码调用,保护模式下增加了门的概念。
门,指向某个优先级高的程序所规定的入口点,所有优先级低的程序调用优先级高的程序只能通过门重定向,进入门的规定的入口点。这样可以避免低级别的程序代码从任意位置进入优先级高的程序的问题。
保护模式下的中断和异常等服务程序也要从门进入,80386的门分为中断门、自陷门和任务门几种。
在保护模式下要表示一个中断或异常服务程序的信息需要用8个字节,包括门的种类以及xxxx:yyyyyyyy格式的入口地址等。这组信息叫做中断描述符。这样,中断向量表就无法采用和实模式下同样的4字节一组的格式。
保护模式下把所有的中断描述符放在一起组成“中断描述符表”IDT(Interrupt Descriptor Table)。IDT不再放在固定的地址00000h处,而是采用可编程设置的方式,支持的中断数量也可以设置。为此80386处理器引入了一个新的48位寄存器IDTR。IDTR的高32位指定了IDT在内存中的基址(线性地址),低16位指定了IDT的长度,相当于指定了可以支持的中断数量。
保护模式下发生异常或中断时,处理器先根据IDTR寄存器得到中断描述符的地址,然后取出n号中断/异常的门描述符,再从描述符中得到中断服务程序的地址xxxx:yyyyyyyy,经过段地址转换后得到服务程序的32位线性地址并转移后执行。
由于保护模式下用中断门可以从低优先级的代码调用高优先级的代码,所以不能让用户程序写中断描述符表,否则会引发安全问题(CIH病毒)。这样就如同关了窗子挡住苍蝇,也挡住了微风,用户的系统扩展程序也就不能像在DOS中一样再用中断服务程序的方式提供服务了。因为用户程序根本没有权限将中断地址指到自己的代码中来。
在Windows中,操作系统使用动态链接库来代替中断服务程序提供系统功能,所以Win32汇编中int指令也就失去了存在的意义。这就是在Win32汇编源代码中看不到int指令的原因。其实那些调用API的指令原本是用int指令实现的。
80386的保护机制
80286之前的处理器只支持单任务,操作系统并没有什么安全性可言,计算机的全部资源包括操作系统的内部都可以任凭程序员调用。
但对于多任务的操作系统,某个捣乱的程序的为所欲为令即可使所有程序都无法运行。
所以80286及以上的处理器引入了优先级的概念。80386处理器共设置4个优先级(0~3)。0级是最高级(特权级);3级是最低级(用户级);1级和2级介于它们之间。特权级代码一般是操作系统的代码,可以访问全部系统资源;其他级别的代码一般都是用户程序,可以访问的资源受到限制。
80386采用保护机制主要为了检查和防止低级别代码的越权操作,如访问不该访问的数据、端口以及调用高优先级的代码等。保护机制主要由下列几方面组成:
1)段的类型检查——段的类型是由段描述符指定的,主要属性有是否可执行,是否可读和是否可写等。而CS、DS和SS等段选择器是否能装入某种类型的段描述符是有限制的。如不可执行的段不能装入CS;不可读的段不能装入DS与ES等数据段寄存器;不可写的段不能装入SS等。如果段类型检查通不过,则处理器会产生一般性保护异常或堆栈异常。
2)页的类型检查——除了可以在段级别上指定整个段是否可以读写外,在页表中也可以为每个页指定是否可写。对特权级下的执行代码,所有的页都是可写的。但对1,2和3级的代码,还要根据页表中的R/W项决定是否可写,企图对只读的页进行写操作会产生页异常。
3)访问数据时的级别检查——优先级低的代码不能访问优先级高的数据段。
80386的段描述符中有一个DPL域(描述符优先级),表示这个段可以被访问的最低优先级。而段选择器中含有RPL域(请求优先级),表示当前执行代码的优先级。只有DPL在数值上大于或等RPL值的时候,该段才是可以访问的,否则会产生一般性保护异常。
4)控制转移的检查——在处理器中,有很多指令可以实现控制转移,如jmp,call,ret,int和iret等指令。但优先级低的代码不能随意转移到优先级高的代码中,所以遇到这些指令的时候,处理器要检查转移的目的位置是否合法。
5)指令集的检查——有两类指令可以影响保护机制。
第一类是改变GDT,LDT,IDT以及控制寄存器等关键寄存器的指令,称为特权指令; 第二类是操作I/O端口的指令以及cli和sti等改变中断允许的指令,称为敏感指令。试想一下,如果用户程序可以用sti禁止一切中断(包括时钟中断),那么整个系统就无法正常运行,所以这些指令的运行要受到限制。特权指令只能在优先级0上才能运行,而敏感指令取决于eflags寄存器中的IOPL位。只有IOPL位表示的优先级高于当前代码段的优先级时,指令才能执行。
6)I/O操作的保护——I/O地址也是受保护的对象。因为通过I/O操作可以绕过系统对很多硬件进行控制。80386可以单独为I/O空间提供保护,每个任务有个TSS(任务状态段)来记录任务切换的信息。TSS中有个I/O允许位图,用来表示对应的I/O端口是否可以操作。某个I/O地址在位图中的对应数据位为0则表示可以操作;如果为1则还要看eflags中的IPOL位,这时只有IOPL位表示的优先级高于等于当前代码段的优先级,才允许访问该I/O端口。
Windows的保护机制
在Windows下,操作系统运行于0行,应用程序运行于3级。因为Alpha计算机只支持两个优先级,为了便于将应用程序移植到Alpha计算机上,Windows操作系统不使用1和2级这两个优先级。
Windows操作系统充分利用80386的保护机制,所有和操作系统密切相关的东西都是受保护的。运行于优先级3上的用户程序有很多限制,只有在写VxD等驱动程序的时候才可以使用全部资源。在Win32汇编编程中要注意避免以下的越权操作(当然写驱动程序不在此列):
1)显而易见,所有的特权指令都是不可执行的,如lgdt,lldt,lidt指令和对CRx与TRx等寄存器赋值。但是,读取重要寄存器的指令是可以执行的,如sgdt,sldt和sidt等。
2)Windows在页表中把代码段和数据段中的内存赋予不同的属性。代码段是不可写的,数据段中也只有变量部分的页面是可写的。所以虽然可以寻址所有的4GB空间,但访问越出权限规定以外的东西还是会引发保护异常。
3)在Windows 98中,系统硬件用的I/O端口是受保护的,但其余的则可以操作。如果用户在机器中插入一块自己的卡,用的是300h系统未定义的端口,那么在应用程序中就可以直接操作,但要操作3f8h(串口)和1f0h(硬盘端口)等系统已定义的端口就不行了。在Windows NT中,任何的端口操作都是不允许的。
如果违反了Windows规定的“保护条例”,那么会引发保护异常,处理器会毫不犹豫地把控制权转移到对应的异常处理程序中去。Windows会在处理程序中用一个很酷的“非法操作”对话框把用户的程序判死刑,没有一点回旋的余地!在Windows 9x中,系统有时会用一个蓝屏幕来通知用户程序试图访问不存在的内存页。
如果程序调用的DLL中有错,那么错误还是会算在应用程序头上,因为DLL的地址空间是被映射到应用程序的空间中去的。Windows 9x本身是32位和16位混合的操作系统,为了兼容DOS和Win16程序,很多的保护措施做起来力不从心。所以系统内部反而常常出现越权操作,以至于蓝屏幕不断,这些就不是用户应用程序自己的问题了。