1.7 Intel 80x86处理器简介

到此,您已经看到了两个将要实际编译和运行的HLA程序。然而至今为止,出现在程序中的所有语句要么是数据声明,要么是对HLA标准库例程的调用,还不是真正的汇编语言。在我们学习真正的汇编语言之前,必须理解Intel 80x86处理器系列的基本结构,否则机器指令将没有任何意义。

Intel 系列CPU一般都被归为冯·诺依曼式机器。冯·诺依曼计算机系统包括三个主要模块:中央处理单元(CPU),存储器和输入/输出设备(I/O)。这三部分通过系统总线(由数据、地址和控制总线组成)相连。图1-4中的模块图表示了这种关系。




图1-4 冯·诺依曼计算机系统模块图

CPU与存储器和I/O设备之间的通信方法是向地址总线发送一个数值来选取一个存储单元或者I/O设备端口,它们都有惟一的二进制地址。因此,CPU、I/O以及存储器设备都通过将数据放到数据总线上来传递彼此之间的数据。控制总线则包含用于确定数据传输方向(在内存之间,以及I/O设备之间)的信号。

在CPU当中,寄存器是其最主要的特征。80x86 CPU的寄存器可以分成四类:通用寄存器、特殊目的寄存器、段寄存器以及特殊目的核心模式寄存器。本书不会介绍最后两类寄存器。段寄存器在现代的32位操作系统(例如:Windows,BeOS和Linux)当中用得不多;由于本书是专门针对32位操作系统所编写的程序,所以不需要讨论段寄存器。特殊目的核心模式寄存器专门用来编写操作系统、调试器以及其他系统级工具。这些软件的构建远远超出了本书的范围,所以也无需讨论特殊目的核心模式寄存器。

80x86(Intel系列)CPU提供了几个通用寄存器。其中包含八个32位寄存器,如下所示:

EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP

每个名称的前缀‘E’代表扩展(extended)的意思。该前缀将32位寄存器与如下所示的八个16位寄存器区分开来:

AX、BX、CX、DX、SI、DI、BP、SP

最后,80x86 CPU还提供了八个8位的寄存器,它们的名称如下所示:

AL、AH、BL、BH、CL、CH、DL、DH

遗憾的是,这些并非都是分离的寄存器。即80 x 86并未提供24个独立的寄存器。实际上,80x86将16位寄存器重叠于32位寄存器之上,也将8位寄存器重叠于16位寄存器之上。图1-5给出了这种关系。




图1-5 80x86(Intel系列CPU)的通用寄存器

关于通用寄存器,需要注意的是它们并不独立。修改一个寄存器可能会影响其他三个寄存器。例如,对EAX的修改可能会影响寄存器AL,AH以及AX。这一点相当重要。在汇编语言初学者所编写的程序当中,一个很常见的错误就是寄存器值的破坏,因为这些编程人员尚未完全理解图1-5的含义。

EFLAGS是一个32位寄存器,它包含几个1位的布尔值(true/false)。EFLAGS寄存器中的大多数要么是为核心模式(操作系统)函数保留的,要么就是程序员不感兴趣的。其中有8位(或标志)是程序员编写汇编语言程序需要用到的。它们分别是溢出标志、方向标志、中断禁止[4]标志、符号标志、零标志、辅助进位标志、奇偶标志以及进位标志。图1-6显示了EFLAGS寄存器低16位中标志的布局情况。

在应用程序员感兴趣的8个标志中,有4个标志特别重要:溢出标志、进位标志、符号标志以及零标志。这4个标志统称为条件码[5]。根据这些标志的状态就可以测试前一次计算的结果。例如,在对两个值进行比较以后,条件码标志就会告诉您其中一个值是小于,等于还是大于另一个值。




图1-6 标志寄存器的布局(EFLAGS的低16位)

令汇编语言初学者惊讶的一个重要事实是,在80x86 CPU中进行的所有计算几乎都和寄存器有关。例如,将两个变量相加,把它们的和存入第3个变量中,必须先将其中一个变量装入一个寄存器中,并将第2个操作数和寄存器的值相加,然后将寄存器中的值存入目的变量。寄存器在每次计算中都充当媒介。因此,在80x86汇编语言程序中寄存器是非常重要的。

还有一点很重要,虽然某些寄存器被称为是“通用”的,但并不能就此推断每个寄存器都能随意使用。以寄存器对SP/ESP为例,它们就有特殊的目的,而不能用于任何其他的目的(它们是栈指针)。同样,寄存器BP/EBP也具有特殊目的,它们的使用受到限制而无法当作通用寄存器使用。所有80x86寄存器都有它们自己特殊的目的,因此它们都限定在一个范围内使用。目前,您应该避免将ESP和EBP寄存器用于一般的计算当中;还应该记住,余下的寄存器在程序中不可以完全互换。存储子系统

运行32位操作系统的80x86处理器可以访问多达232个不同的存储单元,或者四十多亿字节。在几年以前,4G字节的存储器是最大的;然而,现代的机器已经远远超出了这一限制。由于在使用像Windows和Linux这样的32位操作系统时,80x86体系结构支持最大4G字节的地址空间,下面的讨论将假设最大有4G字节的空间。

当然,您应该问的第一个问题是:“确切地说,什么是一个存储单元?”80x86支持字节可寻址存储器(byte addressable memory)。因此基本的存储单元就是一个字节,这足以容纳一个单字符或是一个(非常)小的整数值(第2章将更详细地讨论这一点)。

将内存看作是字节线性数组。首字节的地址为0,末字节的地址为。对于奔腾处理器来说,下面的伪Pascal数组声明就是一个对内存非常好的比拟:

Memory: array [0..4294967295] of byte;

C/C++和Java用户可能偏爱下面这种语法:

byte Memory[4294967296];

为了执行Pascal语句“Memory[125]:=0;”的等效语句,CPU将数值0放在数据总线上,将地址125放到地址总线上,并设置写信号线(通常要将该线置0),如图1-7所示。




图1-7 存储器的写操作

若执行“CPU:=Memory[125];”的等效语句,CPU会将地址125放到地址总线上,并设置读信号线(因为CPU正在从存储器读取数据),然后从数据总线上读取数据结果(如图1-8所示)。




图1-8 存储器的读操作

这里的讨论只适用于访问内存中单个字节的情况。那么当处理器访问一个字或者双字时又会如何呢?由于存储器由一个字节数组构成,那么我们应该怎样处理大于一个字节的数据呢?很简单,要存储更大的值,80x86使用连续的存储单元序列就可以了。图1-9给出了80x86如何在存储器中存储字节、字(双字节)以及双字(四字节)。每个对象的内存地址就是它的首字节地址(最低地址)。

现代的80x86处理器实际上并不直接与存储器相连。而是CPU上有一个专门的存储缓冲器称为高速缓存(cache),它充当CPU与主存储器之间的高速媒介。虽然高速缓存可以自动处理细节,但应该明白一个事实,即如果该对象的地址为对象长度的整数倍,那么访问内存中的数据对象的效率更高。因此,用4的整数倍的地址来对准四字节(双字)对象的确是个好主意。同样,让双字节对象对准偶数地址是最有效的。在后面的章节中,您将学习如何设置内存对象的对准。




图1-9 存储器中字节、字和双字的存储

在结束讨论内存对象以前,应该理解内存与HLA变量之间的相关性。使用像HLA这样的汇编器/编译器的一个好处是不必担心内存地址值。只需在HLA中声明一个变量,HLA负责将该变量与惟一一组内存地址集合相关联。例如,对于下面的声明段:

static

i8 :int8;

i16 :int16;

i32 :int32;

HLA将在内存中找到某个未使用的8位字节,将其与变量i8相关联;还将找到一对连续未使用的字节,并将它们与i16相关联。最后,HLA将找到4个连续未使用的字节,并将i32的值与这4个字节(32位)相关联。以后就要通过变量的名称来引用它们,而不必关心它们的地址值。但是,您仍然应该知道HLA在后台完成了这些工作。