WIN32汇编语言教程:第01章 背景知识 · 1.3 必须了解的东西(4)
图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的寻址空间,而且这个空间是“私有”的。
● DLL程序没有自己“私有”的空间。它们总是被映射到其他应用程序的地址空间中,当做其他应用程序的一部分运行。原因很简单,如果它不和其他程序同属一个地址空间,应用程序该如何调用它呢?
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的门分为中断门、自陷门和任务门几种。