⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 001.txt

📁 会变语言实现的一些程序
💻 TXT
📖 第 1 页 / 共 4 页
字号:

5. 从Win32汇编的角度看内存寻址

对初学者来说,DOS下的分段寻址方式就已经令人一头雾水了,80386保护模式的内存管理就更麻烦。的确,如果在Win32汇编中访问内存之前要先在描述符表中构造正确的描述符,然后再构造页表把物理内存映射到要访问的线性地址的话,那就简直是一场噩梦,有90%的汇编程序员会因此改行去卖茶叶蛋!

但实际上这并没有发生,因为Win32汇编中的内存访问远比DOS下的分段寻址方式简单,这是为什么呢?

因为Windows是一个多任务的操作系统,最首要的宗旨就是“稳定压倒一切”。但如果把描述符表以及页表等内容交给用户程序是很不安全的,不用说全局描述符表,就是为每个程序建立的局部描述符表也不应该让用户程序改写,否则用户可以通过构造自己的描述符来访问操作系统不希望用户访问的东西。任何权限上开放引发的安全问题都是很严重的,如Windows 9x中的中断描述符表是可写的,CIH病毒可利用它将自己的权限提高到优先级0;而Windows NT下的中断描述符表是不可写的,CIH病毒在Windows NT下就无法进驻内存。

正因为如此,Windows操作系统干脆为用户程序“安排好了一切”。具体表现在为用户程序的代码段、数据段和堆栈段全部预定义好了段描述符。这些段的起始地址为0,限长为ffffffff,所以用它们可以直接寻址全部的4 GB地址空间。程序开始执行的时候,CS,DS,ES和SS都已经指向了正确的描述符,在整个程序的生命周期内,程序员不必改动这些段寄存器,也不必关心它们的值究竟是多少(实际上,想改也改不了)。

所以对Win32汇编程序来说,整个源程序中竟然可以不用出现段寄存器的身影。这在DOS汇编编程中是不可想像的。回顾本节开头提出的问题,答案是:并不是Win32汇编源代码用不到段寄存器,而是用户在使用中不必去关心段寄存器!

1.3.3  Windows的特权保护

Windows的特权保护和处理器硬件的支持是分不开的。优先级的划分、指令的权限检查和超出权限访问的异常处理等是构成特权保护的基础。这一节将简单介绍这些课题,读者可以考虑一下初学Win32汇编时遇到的疑问:

● Win32汇编中为什么找不到中断指令的使用?

● Windows错误的“蓝屏幕”是从哪里来的?

1. 80386的中断和异常

中断指当程序执行过程中有更重要的事情需要实时处理时(如串口中有数据到达,不及时处理数据会丢失,串行控制器就提交一个中断信号给处理器要求处理),硬件通过中断控制器通知处理器。处理器暂时挂起当前运行的程序,转移到中断处理程序中;当中断处理程序处理完毕后,通过iret指令回到原先被打断的程序中继续执行。

异常指指令执行中发生不可忽略的错误时(如遇到无效的指令编码,除法指令除零等),处理器用和中断处理相同的操作方法挂起当前运行的程序转移到异常处理程序中。异常处理程序决定在修正错误后是否回到原来的地方继续执行。

更为DOS汇编程序员熟悉的“中断”指的是用int n指令直接转移到中断向量n指定的中断处理程序中执行。严格地讲,int n指令应该算“自陷”而不是“中断”。因为这时并不是程序被急需解决的事情打断。而是自己要求停止执行并转移到中断处理程序中去。

不管中断、异常还是自陷,虽然它们产生的原因不同,但处理过程是类似的,都通过中断向量表里存放的入口地址转移到服务程序,都由CPU自动在堆栈中保护断点地址,最后也都可以用iret指令返回指令被中断的地方。

先回顾一下8086或80386实模式下中断和异常的处理过程。如图1.7所示,实模式下的中断和异常服务程序地址存放在中断向量表中。中断向量表位于物理内存00000h开始的400h字节中,共支持100h个中断向量;每个中断向量是一个xxxx:yyyy格式的地址,占用4字节。当发生n号异常或n号中断,或者执行到int n指令的时候,CPU首先到内存n×4的地方取出服务程序的地址aaaa:bbbb(图示步骤①);然后将标志寄存器、中断时的CS和IP压入堆栈,接着转移到aaaa:bbbb处执行(步骤②);在服务程序最后遇到iret的时候,CPU从堆栈中恢复标志寄存器,然后取出CS和IP并返回。


图1.7  实模式下中断和异常的处理

在保护模式下,中断或异常处理往往从用户代码切换到操作系统代码中执行。由于保护模式下的代码有优先级之分,因此出现了从优先级低的应用程序转移到优先级高的系统代码中的问题,如果优先级低的代码能够任意调用优先级高的代码,就相当于拥有了高优先级代码的权限。为了使高优先级的代码能够安全地被低优先级的代码调用,保护模式下增加了“门”的概念。“门”指向某个优先级高的程序所规定的入口点,所有优先级低的程序调用优先级高的程序只能通过门重定向,进入门所规定的入口点。这样可以避免低级别的程序代码从任意位置进入优先级高的程序的问题。保护模式下的中断和异常等服务程序也要从“门”进入,80386的门分为中断门、自陷门和任务门几种。

 

在保护模式下要表示一个中断或异常服务程序的信息需要用8个字节,包括门的种类以及xxxx:yyyyyyyy格式的入口地址等。这组信息叫做“中断描述符”。这样,中断向量表就无法采用和实模式下同样的4字节一组的格式。保护模式下把所有的中断描述符放在一起组成“中断描述符表”IDT(Interrupt Descriptor Table)。IDT不再放在固定的地址00000h处,而是采用可编程设置的方式,支持的中断数量也可以设置。为此80386处理器引入了一个新的48位寄存器IDTR。IDTR的高32位指定了IDT在内存中的基址(线性地址),低16位指定了IDT的长度,相当于指定了可以支持的中断数量。

如图1.8所示,保护模式下发生异常或中断时,处理器先根据IDTR寄存器得到中断描述符的地址,然后取出n号中断/异常的门描述符,再从描述符中得到中断服务程序的地址xxxx:yyyyyyyy,经过段地址转换后得到服务程序的32位线性地址并转移后执行。


图1.8  保护模式下的中断和异常处理

由于保护模式下用中断门可以从低优先级的代码调用高优先级的代码,所以不能让用户程序写中断描述符表,否则会引发安全问题(又想到了CIH病毒)。这样就如关了窗子挡住苍蝇,也挡住了微风,用户的系统扩展程序也就不能像在DOS中一样再用中断服务程序的方式提供服务了。因为用户程序根本没有权限将中断地址指到自己的代码中来。

在Windows中,操作系统使用动态链接库来代替中断服务程序提供系统功能,所以 Win32汇编中int指令也就失去了存在的意义。这就是在Win32汇编源代码中看不到int指令的原因。其实那些调用API的指令原本是用int指令实现的。

2. 80386的保护机制

80286之前的处理器只支持单任务,操作系统并没有什么安全性可言,计算机的全部资源包括操作系统的内部资源都可以任凭程序员调用。但对于多任务的操作系统,某个捣乱的程序为所欲为令使所有程序都无法运行。所以80286及以上的处理器引入了优先级的概念。80386处理器共设置4个优先级(0~3)。0级是最高级(特权级);3级是最低级(用户级);1级和2级介于它们之间。特权级代码一般是操作系统的代码,可以访问全部系统资源;其他级别的代码一般是用户程序,可以访问的资源受到限制。

80386采用保护机制主要为了检查和防止低级别代码的越权操作,如访问不该访问的数据、端口以及调用高优先级的代码等。保护机制主要由下列几方面组成:

● 段的类型检查——段的类型是由段描述符指定的,主要属性有是否可执行,是否可读和是否可写等。而CS,DS和SS等段选择器是否能装入某种类型的段描述符是有限制的。如不可执行的段不能装入CS;不可读的段不能装入DS与ES等数据段寄存器;不可写的段不能装入SS等。如果段类型检查通不过,则处理器会产生一般性保护异常或堆栈异常。

● 页的类型检查——除了可以在段级别上指定整个段是否可读写外,在页表中也可以为每个页指定是否可写。对于特权级下的执行代码,所有的页都是可写的。但对于1,2和3级的代码,还要根据页表中的R/W项决定是否可写,企图对只读的页进行写操作会产生页异常。

● 访问数据时的级别检查——优先级低的代码不能访问优先级高的数据段。80386的段描述符中有一个DPL域(描述符优先级),表示这个段可以被访问的最低优先级。而段选择器中含有RPL域(请求优先级),表示当前执行代码的优先级。只有DPL在数值上大于或等于RPL值的时候,该段才是可以访问的,否则会产生一般性保护异常。

● 控制转移的检查——在处理器中,有很多指令可以实现控制转移,如jmp,call,ret,int和iret等指令。但优先级低的代码不能随意转移到优先级高的代码中,所以遇到这些指令的时候,处理器要检查转移的目的位置是否合法。

● 指令集的检查——有两类指令可以影响保护机制。第一类是改变GDT,LDT,IDT以及控制寄存器等关键寄存器的指令,称为特权指令;第二类是操作I/O端口的指令以及cli和sti等改变中断允许的指令,称为敏感指令。试想一下,如果用户级程序可以用sti禁止一切中断(包括时钟中断),那么整个系统就无法正常运行,所以这些指令的运行要受到限制。特权指令只能在优先级0上才能运行,而敏感指令取决于eflags寄存器中的IOPL位。只有IOPL位表示的优先级高于等于当前代码段的优先级时,指令才能执行。

● I/O操作的保护——I/O地址也是受保护的对象。因为通过I/O操作可以绕过系统对很多硬件进行控制。80386可以单独为I/O空间提供保护,每个任务有个TSS(任务状态段)来记录任务切换的信息。TSS中有个I/O允许位图,用来表示对应的I/O端口是否可以操作。某个I/O地址在位图中的对应数据位为0则表示可以操作;如果为1则还要看eflags中的IPOL位,这时只有IOPL位表示的优先级高于等于当前代码段的优先级,才允许访问该I/O端口。

3. Windows的保护机制

在Windows下,操作系统运行于0级,应用程序运行于3级。因为Alpha计算机只支持两个优先级,为了便于将应用程序移植到Alpha计算机上,Windows操作系统不使用1和2级这两个优先级。

Windows操作系统充分利用80386的保护机制,所有和操作系统密切相关的东西都是受保护的。运行于优先级3上的用户程序有很多限制,只有在写VxD等驱动程序的时候才可以使用全部资源。在Win32汇编编程中要注意避免以下的越权操作(当然写驱动程序不在此列):

● 显而易见,所有的特权指令都是不可执行的,如lgdt,lldt,lidt指令和对CRx与TRx等寄存器赋值。但是,读取重要寄存器的指令是可以执行的,如sgdt,sldt和sidt等。

● Windows在页表中把代码段和数据段中的内存页赋予不同的属性。代码段是不可写的,数据段中也只有变量部分的页面是可写的。所以虽然可以寻址所有的4 GB空间,但访问超出权限规定以外的东西还是会引发保护异常。

● 在Windows 98中,系统硬件用的I/O端口是受保护的,但其余的则可以操作。如果用户在机器中插了一块自己的卡,用的是300h等系统未定义的端口,那么在应用程序中就可以直接操作,但要操作3f8h(串口)和1f0h(硬盘端口)等系统已定义的端口就不行了。在Windows NT中,任何的端口操作都是不允许的。

如果违反了Windows规定的“保护条例”,那么会引发保护异常,处理器会毫不犹豫地把控制权转移到对应的异常处理程序中去。Windows会在处理程序中用一个很酷的“非法操作”对话框把用户的程序判死刑,没有一点回旋的余地!在Windows 9x中,系统有时会用一个蓝屏幕来通知用户程序试图访问不存在的内存页。

如果程序调用的DLL中有错,那么错误还是会算在应用程序头上,因为DLL的地址空间是被映射到应用程序的空间中去的。Windows 9x本身是32位和16位混合的操作系统,为了兼容DOS和Win16程序,很多的保护措施做起来力不从心。所以系统内部反而常常出现越权操作,以至于蓝屏幕不断,这些就不是用户应用程序自己的问题了。
 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -