编程卓越之道(第二卷)汇编语言摘

3.3  80x86基本架构
Intel的CPU一般归类于冯·诺依曼(Von Neumann)结构。冯·诺依曼计算机系统主要包含3大部分:中央处理单元( CPU)、内存和输入/输出( I/O)设备。这3个组件通过系统总线(包括地址总线、数据总线和控制总线)相连。图3-1给出了其相互关系。
图3-1  冯·诺依曼计算机系统模块图
CPU向地址总线放入地址值,以此选择内存单元位置或I/O设备端口位置,从而与内存和外设通信。这些内存单元或端口位置在系统中占有唯一的二进制地址值。然后CPU、内存和I/O设备通过数据总线互相传递数据。控制总线里的信号线控制着数据进出内存或I/O设备的方向。
3.3.1  寄存器
寄存器组是CPU内部最突出的特性。几乎所有的80x86 CPU操作都涉及至少一个寄存器。例如,要将两个变量的值加起来,结果放入第三个变量中,必须将其中一个变量放入寄存器,对其加上第二个操作数,然后将结果送入目标变量。寄存器差不多是每个操作的中介。因此在80x86汇编语言程序中,寄存器是必不可少的。
80x86 CPU的寄存器可以分成4类:通用寄存器、特殊目的寄存器(应用可访问)、段寄存器和特殊目的核心模式寄存器。由于段寄存器在Windows、BSD、BeOS和Linux等现代32位操作系统中不怎么用,而特殊目的核心模式寄存器是用来编写操作系统、调试器和其他系统级工具的,这些软件的构建方法已超出了本书范围,所以我们不再考虑段寄存器和特殊目的核心模式寄存器。
3.3.2  80x86通用寄存器
Intel的80x86 CPU家族为应用程序提供若干通用寄存器,包括如下8个32位寄存器:
EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP
每个寄存器名的“E”前缀表示“扩展( Extended)”,以便将这些32位寄存器与下列16位寄存器区分开来:
AX、BX、CX、DX、SI、DI、BP、SP
80x86 CPU还提供如下8个8位寄存器:
AL、AH、BL、BH、CL、CH、DL、DH
特别要注意的是,这些通用寄存器并非各为一体,即80x86并不提供24个单独的寄存器,而是将32位寄存器与16位寄存器重叠,后者又重叠以8位寄存器。图3-2给出了其相互关系:
图3-2  Intel 80x86 CPU的通用寄存器
因此实际修改某个寄存器内容时,可能没有特别指明就改动了三个寄存器的值。例如,修改寄存器EAX也许会改变寄存器AL、AH和AX的值。我们将会经常看到编译器所产生的代码用到80x86的这种特性。举个例子,编译器会清除(置0)寄存器EAX的所有位,然后将AL设为1或0,以便得到一个32位的True(1)或False(0)值。有的
机器指令只操纵AL寄存器,而程序也许需要以EAX返回这些指令的结果。利用寄存器重叠的优点,编译器产生的代码就能够通过操纵AL的指令而返回整个EAX值。
虽然Intel将这些寄存器称作“通用寄存器”,但不能因此就以为它们可用于任何目的。比如说,寄存器SP/ESP就有着专门用途,切勿挪作它用,因为它是堆栈指针。类似地,寄存器BP/EBP也是专用的,无法当作通用寄存器使用。所有80x86寄存器都有各自的特殊意图,仅可在特定环境下使用。在讨论使用这些寄存器的机器指令时,我们会考虑这些特殊用法的,请参看在线资源。
3.3.3  80x86的EFLAGS寄存器
32位EFLAGS寄存器将许多单一比特位的布尔值(True/False)或标志位集合在一起。这些比特位中的大部分要么为操作系统的核心模式函数保留,要么与应用程序员没有太大关系。应用程序员只要会以汇编语言代码读写其中8位即可——溢出标志位、方向标志位、中断禁止位 [1]、符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位。图3-3展示了这些标志位在寄存器EFLAGS低16位中的布放位置。
图3-3  80x86标志寄存器低16位的布局
应用程序员可用的这8个标志位中,4个具有特别价值:溢出标志位、进位标志位、符号标志位和零标志位。我们将这4个标志位称为“条件码( condition code)”。每个标志位都有一个状态——要么是设置(1),要么是清除(0),可以用来检验上次运算的结果。例如在比较两个值后,条件码标志可告诉我们其中一个值是小于、等于还是大于另一个值。

3.6  80x86的寻址模式

“寻址模式(addressing mode)”是为了访问指令操作数而设置的特定于某种硬件的机制。80x86家族提供3种不同类别的操作数:寄存器操作数、立即操作数及内存操作数。下面几节将讨论这些寻址模式。

3.6.1  80x86的寄存器寻址

80x86指令大都能够操作80x86的通用寄存器。可以将寄存器的名字作为指令操作数来访问该寄存器。

我们用80x86的mov指令来看一些示例,了解汇编器是如何实现这种策略的。

3.6.1.1  HLA中的寄存器访问

HLA的mov指令看上去是这样的:

mov( source, destination );

该指令将操作数source中的数据复制到操作数destination。8位、16位和32位寄存器都可以作为该指令的有效操作数。对两个操作数的唯一限制就是要求其位数相同。

我们来看一些实际的80x86 mov指令:

mov( bx, ax );    // 将BX的内容传送到AX

mov( al, dl );    // 将AL的内容传送到DL

mov( edx, esi ); // 将EDX的内容传送到ESI

3.6.1.2  Gas中的寄存器访问

Gas中的每个寄存器名要冠以百分号“%”,例如:

%al, %ah, %bl, %bh, %cl, %ch, %dl, %dh

%ax, %bx, %cx, %dx, %si, %di, %bp, %sp

%eax, %ebx, %ecx, %edx, %esi, %edi, %ebp, %esp

Gas的mov指令语法与HLA雷同,只是无需括号和分号,并且要求其汇编语句在源代码中不能跨行。例如:

mov %bx, %ax      // 将BX的内容传送到AX

mov %al, %dl      // 将AL的内容传送到DL

mov %edx, %esi    // 将EDX的内容传送到ESI

3.6.1.3  MASM和TASM中的寄存器访问

MASM和TASM汇编器使用的寄存器名与HLA相同,基本语法则类似于Gas,但源和目标操作数须互换位置。也就是说,典型的mov指令是这样:

mov destination, source

这里有一些MASM和TASM语法的mov指令示例:

mov ax, bx     ; 将BX的内容传送到AX

mov dl, al     ; 将AL的内容传送到DL

mov esi, edx   ; 将EDX的内容传送到ESI

3.6.2  立即寻址

将寄存器和内存作为操作数的指令多数也能用立即数,即以常量为操作数。例如HLA 的下列mov指令将数值送入相应的目标寄存器:

mov( 0, al );

mov( 12345, bx );

mov( 123_456_789, ecx );

采用立即寻址模式时,大部分汇编器都允许指定各种各样的文字常量类型。例如,可以提供十六进制、十进制和二进制形式的数;也可以提供字符常量作为操作数。原则是常量必须在目标操作数中放得下。下面是一些HLA、Gas和MASM/TASM的例子:

mov( 'a', ch );   // HLA

mov 'a, %ch       // Gas

mov ch, 'a'       ;MASM/TASM

mov( $1234, ax ); // HLA

mov 0x1234, %ax   // Gas

mov ax, 1234h     ;MASM/TASM

mov( 4_012_345_678, eax ); // HLA

mov 4012345678, %eax      // Gas

mov eax, 4012345678       ;MASM/TASM

几乎每个汇编器都允许创建符号常量名,并将这些常量名当作操作数。例如,HLA预定义了两个布尔常量true和false,我们可以将其作为mov指令的操作数:

mov( true, al );

mov( false, ah );

某些汇编器甚至可用指针常量等抽象数据类型的常量,请参看对应汇编器的手册来了解用法细节。

3.6.3  位移寻址

最常见且最易理解的寻址模式是位移寻址,即直接寻址,其中通过32位常量指定内存单元的位置。内存单元可以是源操作数,也可以是目标操作数。

举个例子,假设变量J是位于地址$8088的字节变量,HLA指令“mov (J, al);”则指的是将位于内存地址$8088中的内容传送入寄存器AL中。类似地,如果字节变量K在内存中位于$1234,指令“mov (dl, K);”则指的是把寄存器DL中的内容传送到地址单元$1234中,请参看图3-4。

位移寻址模式非常合适于访问简单的标量变量。高级语言程序中通常使用这种寻址模式访问静态变量或全局变量。

注意:Intel将这种寻址模式称作“位移寻址”,是因为操作码mov后面跟着32位常量的内存地址(即位移值)。在80x86处理器上,位移值是相对于内存起始位置(即地址0)的偏移量。

图3-4  位移寻址模式(直接寻址)

本章中的示例通常访问内存中的字节型数据。但也要知道,在80x86处理器上通过指定第一个字节的地址,同样可以访问字和双字,请参看图3-5。

图3-5  使用直接寻址模式访问字或双字型数据

MASM、TASM和Gas对于位移寻址模式所采用的语法与HLA相同,即只要指定欲访问的变量名作为操作数即可。有的MASM、TASM程序员爱用方括号括住变量名,尽管在这些汇编器中并无必要这么做。

下面是一些使用HLA、Gas和MASM/TASM语法的示例:

mov( byteVar, ch );   // HLA

movb byteVar, %ch     // Gas

mov ch, byteVar       ;MASM/TASM

mov( wordVar, ax );   // HLA

movw wordVar, %ax     // Gas

mov ax, wordVar       ; MASM/TASM

mov( dwordVar, eax ); // HLA

movl dwordVar, %eax   // Gas

mov eax, dwordVar     ; MASM/TASM

3.6.4  寄存器间接寻址

80x86 CPU能够使用寄存器间接寻址模式,即通过寄存器间接访问内存。我们称这种模式为“间接”,是因为操作数本身并非地址,操作数的值才是要用的内存地址。在寄存器间接寻址模式中,寄存器的值指向要访问的地址。例如,HLA指令“mov (eax, [ebx]);”告诉CPU,将EAX中的内容保存到以EBX值为地址的内存单元中。

3.6.4.1  HLA的寄存器间接寻址

80x86的这种寻址模式有8个形式,使用HLA时是这样的:

mov( [eax], al );

mov( [ebx], al );

mov( [ecx], al );

mov( [edx], al );

mov( [edi], al );

mov( [esi], al );

mov( [ebp], al );

mov( [esp], al );

这8个寻址形式以方括号内寄存器(分别为EAX、EBX、ECX、EDX、EDI、ESI、EBP和ESP)的内容作为偏移量,寻址到相应的内存单元。

注意:寄存器间接寻址模式要求通过32位寄存器指定内存地址,这种寻址模式下不能用8位或16位寄存器指定。

3.6.4.2  MASM和TASM的寄存器间接寻址

MASM和TASM在寄存器间接寻址模式的语法与HLA基本一样——用一对方括号括住寄存器名——只是操作数的顺序在mov指令中正好颠倒。

前面的HLA指令与MASM/TASM等效为:

mov al, [eax]

mov al, [ebx]

mov al, [ecx]

mov al, [edx]

mov al, [edi]

mov al, [esi]

mov al, [ebp]

mov al, [esp]

3.6.4.3  Gas的寄存器间接寻址

Gas则以圆括号括住寄存器名。前面HLA的mov指令其Gas形式如下:

movb (%eax), %al

movb (%ebx), %al

movb (%ecx), %al

movb (%edx), %al

movb (%edi), %al

movb (%esi), %al

movb (%ebp), %al

movb (%esp), %al

3.6.5  变址寻址

在所有地址运算完成后,指令终将访问的内存地址称为“有效地址(effective address)”。变址寻址模式通过将变量地址——也称为“位移(displacement)”或“偏移(offset)”——与32位寄存器值相加,计算出有效地址,这才是指令准备访问的内存地址。所以如果VarName位于地址$1100,而EBX内容为8,则HLA的“mov (VarName[ebx], al);”将会把地址单元$1108的内容放入寄存器AL,请参看图3-6。

图3-6  变址寻址模式

3.6.5.1  HLA的变址寻址

HLA中采用下列变址寻址语法,其中VarName为程序中某个静态变量的名字。

mov( VarName[ eax ], al );

mov( VarName[ ebx ], al );

mov( VarName[ ecx ], al );

mov( VarName[ edx ], al );

mov( VarName[ edi ], al );

mov( VarName[ esi ], al );

mov( VarName[ ebp ], al );

mov( VarName[ esp ], al );

3.6.5.2  MASM和TASM的变址寻址

MASM、TASM的语法与HLA相同,它们还允许多种语法变形。下面是先前变址寻址指令操作数的等效形式,并给出了MASM和TASM支持的变形。

varName[reg32]

[reg32+varName]

[varName][reg32]

[varName+reg32]

[reg32][varName]

varName[reg32+const]

[reg32+varName+const]

[varName][reg32][const]

varName[const+reg32]

[const+reg32+varName]

[const][reg32][varName]

varName[reg32-const]

[reg32+varName-const]

[varName][reg32][-const]

MASM和TASM还允许其他很多组合。这些汇编器将方括号内的并列项当作它们被用加号分隔一样。由于加法满足交换率,组合的种类很多。

这里给出的MASM/TASM语句等效于前节中的HLA示例:

mov al, VarName[ eax ]

mov al, VarName[ ebx ]

mov al, VarName[ ecx ]

mov al, VarName[ edx ]

mov al, VarName[ edi ]

mov al, VarName[ esi ]

mov al, VarName[ ebp ]

mov al, VarName[ esp ]

3.6.5.3  Gas的变址寻址

和寄存器间接寻址相同,Gas在变址寻址模式中仍然用圆括号而非方括号。下面是Gas允许的变址寻址语法:

varName(%reg32)

const(%reg32)

varName+const(%reg32)

前面的HLA指令在Gas中等效为:

movb VarName( %eax ), al

movb VarName( %ebx ), al

movb VarName( %ecx ), al

movb VarName( %edx ), al

movb VarName( %edi ), al

movb VarName( %esi ), al

movb VarName( %ebp ), al

movb VarName( %esp ), al

3.6.6  比例变址寻址

比例变址寻址模式与变址寻址类似,区别只有两点。比例变址寻址还允许:

l  将两个寄存器值相加,外加一个位移量

l  可将变址寄存器的值乘以比例因子1、2、4或8

怎么做呢?请看下面的HLA例子:

mov( eax, VarName[ ebx + esi*4 ] );

比例变址寻址模式与变址寻址的主要差异在于它们有无“esi*4”。该示例通过将EBX加上4倍的ESI值来求得有效地址,图3-7给出了比例变址寻址模式的地址计算过程,其中scale指比例因子,本例中scale等于4。

图3-7  比例变址寻址模式

3.6.6.1  HLA的比例变址寻址

HLA语法提供若干方式来指定使用比例变址寻址模式。这里有一些语法形式:

VarName[ IndexReg32 * scale ]

VarName[ IndexReg32 * scale + displacement ]

VarName[ IndexReg32 * scale - displacement ]

[ BaseReg32 + IndexReg32 * scale ]

[ BaseReg32 + IndexReg32 * scale + displacement ]

[ BaseReg32 + IndexReg32 * scale - displacement ]

VarName[ BaseReg32 + IndexReg32 * scale ]

VarName[ BaseReg32 + IndexReg32 * scale + displacement ]

VarName[ BaseReg32 + IndexReg32 * scale - displacement ]

例子中的BaseReg32表示任意32位通用寄存器,IndexReg32则表示除ESP外的任意32位通用寄存器,scale必须是1、2、4、8中的某个常数,而VarName表示静态变量名。

3.6.6.2  MASM和TASM的比例变址寻址

MASM、TASM支持的语法与HLA相同,但另有一些形式与变址寻址中给出的语法对应。后者只是基于加法交换率的语法变形。

3.6.6.3  Gas的比例变址寻址

Gas还是一如既往使用圆括号而非方括号来括住比例变址操作数。Gas也使用3个操作数的语法来指定基址寄存器、变址寄存器和比例值,不像其他汇编器用算术表达式给出。Gas比例变址寻址表达式的一般语法是:

expression( baseReg32, indexReg32, scaleFactor )

具体有以下形式:

VarName( ,IndexReg32, scale )

VarName + displacement( ,IndexReg32, scale )

VarName - displacement( ,IndexReg32, scale )

( BaseReg32, IndexReg32, scale )

displacement( BaseReg32, IndexReg32, scale )

VarName( BaseReg32, IndexReg32, scale )

VarName + displacement( BaseReg32, IndexReg32, scale )

VarName - displacement( BaseReg32, IndexReg32, scale )

 

3.8  在汇编语言中指定操作数尺寸

80x86上的汇编器通过两种机制指定操作数尺寸:

l  操作数使用类型检查来说明尺寸——多数汇编器都这么做

l  指令本身指定了尺寸——Gas是这么做的

举例来说,我们考虑下列3条HLA的mov指令:

mov( 0, al );

mov( 0, ax );

mov( 0, eax );

每个情况里都由寄存器操作数说明了mov指令传送到那个寄存器的数据尺寸。MASM/TASM采用类似的语法,只是操作数要互相调换位置:

mov al, 0 ;8位数据传送

mov ax, 0 ;16位数据传送

mov eax, 0     ;32位数据传送

一定要注意的是,这6条语句的指令助记符都是一样的,均为mov。正是操作数而非指令助记符指明了要传送数据的尺寸。

3.8.1  HLA的类型强制

不过,这种方法存在一个问题。请考虑下列的HLA示例:

mov( 0, [ebx] );       //将0复制到地址为[ebx]的内存单元

该指令是含糊不清的:EBX指向的内存位置可以是字节、字或双字。指令并未把操作数的尺寸告诉给汇编器。要是遇到这样的指令,汇编器就会报错,我们必须明确指出内存操作数的尺寸。如果是在HLA中,这可以通过类型强制运算符实现:

mov( 0, (type word [ebx]) );   //16位数据传送

通常用下列HLA语法能将任何内存操作数强制成适当尺寸:

(type new_type memory)

其中new_type表示byte、word或dword等数据类型,memory为要强制类型的内存地址。

3.8.2  MASM和TASM的类型强制

MASM和TASM同样存在这一问题。应当这么使用强制运算符来指定内存单元:

mov word ptr [ebx], 0 ;16位数据传送

当然,将例子中的word替换成byte或dword,就能将内存单元强制为字节或双字尺寸。

3.8.3  Gas的类型强制

Gas汇编器无需强制运算符,因为它采用了截然不同的技术来指定操作数的尺寸——由指令助记符明确说明尺寸。Gas的mov指令不只有一个助记符,而是使用三种指令助记符,由mov和一个字符后缀组成,具体有:

movb       传送8位数据(字节)

movw       传送16位数据(字)

movl       传送32位数据(双字)

采用这些指令助记符时,即使操作数尺寸没有明确,也永远不会产生歧义。例如:

movb 0, (%ebx)     //8位数据传送

movw 0, (%ebx)     //16位数据传送

movl 0, (%ebx)     //32位数据传送

你可能感兴趣的:(编程,汇编,语言,byte,编译器,X86)