《汇编语言:基于X86处理器》第12章 浮点数处理与指令编码(2)
Intel X86架构数据的运算主要由通用寄存器处理,但浮点数例外,浮点数的运算由专门的FPU寄存器处理。二进制浮点数由三部分组成:符号,有效数字和阶码。这些格式都出自由IEEE组织制定的标准754-1985:以下是三种浮点数的格式:
●32位单精度数值包含1位符号、8位阶码,以及23位有效数字的小数部分。
●64位双精度数值包含1位符号、11位阶码,以及52位有效数字的小数部分。
●80 位扩展双精度数值包含1位符号、16 位阶码,以及 63 位有效数字的小数部分。
本篇介绍浮点数处理与指令编码。
12.3 x86指令编码
若要完全理解汇编语言操作码和操作数,就需要花些时间了解汇编指令翻译成机器语言的方法。由于Intel 指令集使用了丰富多样的指令和寻址模式,因此这个问题相当复杂。首先以实地址模式的8086/8088为例来说明,之后,再展示32 位处理器带来的变化。
Intel 8086处理器是第一个使用复杂指令集计算机(Complex Instruction Set Computer CISC)设计的处理器。这种指令集中包含了各种各样的内存寻址、移位、算术运算、数据传送和逻辑操作。与RISC(精简指令集计算机,Reduced Instruction Set Computer)指令相比,Intel 指令在编码和解码方面有些复杂。指令编码(encode)是指将汇编语言指令及其操作数转换为机器码。指令解码(decode)是指将机器指令转换为汇编语言。对 Intel 指令编码和解码的逐步解释至少将有助于唤起对MASM 作者们辛苦工作的理解和欣赏。
12.3.1 指令格式
一般的x86机器指令格式(图12-6)包含了一个指令前缀字节、操作码、Mod R/M 字节、伸缩索引字节(SIB)、地址位移和立即数。指令按小端顺序存放,因此前缀字节位于指令的起始地址。每条指令都有一个操作码,而其他字段则是可选的。少数指令包含了全部字段,平均来看,绝大多数指令都有2个或3个字节。下面是对指令字段的简介:
●指令前缀 覆盖默认操作数大小。
●操作码 (操作代码)指定指令的特定变体。比如,按照使用的参数类型,指令 ADD有 9种不同的操作码。
●Mod R/M 字段指定寻址模式和操作数。符号“R/M”代表的是寄存器和模式。表12-18列出了Mod字段,表12-19给出了当Mod=10b时16 位应用程序的R/M 字段。
●伸缩索引字节 (scale indexbyte,SIB)用于计算数组索引偏移量。
●地址位移 字段保存了操作数的偏移量,在基址-偏移量或基址-变址-偏移量寻址模式中,该字段还可以与基址或变址寄存器相加。
●立即数 字段保存了常量操作数。
表12-18 Mod字段取值 | |
Mod | 位移 |
00 | DISP=0,位移低半部分和高半部分都无定义(除非r/m=110) |
01 | DISP=位移低半部分符号扩展到16位,位移高半部分无定义 |
10 | DISP=位移高半部分和低半部分都有效 |
11 | R/M 字段包含的是寄存器编号 |
表12-19 16位R/M字段取值(Mod=10) | |||
R/M | 有效地址 | R/M | 有效地址 |
000 | [BX+SI]+D16 | 100 | [SI]+D16 |
001 | [BX+DI]+D16 | 101 | [DI]+D16 |
010 | [BP+SI]+D16 | 110 | [BP]+D16 |
011 | [BP+DI]+D16 | 111 | [BX]+D16 |
D16表示偏移量是16位的。
12.3.2 单字节指令
没有操作数或只有一个隐含操作数的指令是最简单的指令。这种指令只需要操作码字段,字段值由处理器的指令集预先确定。表12-20列出了几个常见的单字节指令。在这些指令中,INCDX指令好像是不应该出现的,它出现的原因是:指令集的设计者决定为某些常用指令提供独特的操作码。其结果是,为了代码量和执行速度要对寄存器增量操作进行优化。
表12-20 单字节指令 | |||
指令 | 操作码 | 指令 | 操作码 |
AAA | 37 | LODSB | AC |
AAS | 3F | XLAT | D7 |
CBW | 98 | INC DX | 42 |
12.3.3 立即数送寄存器
立即操作数(常数)按照小端顺序(起始地址为最低字节)添加到指令。首先关注的是立即数送寄存器指令,暂不考虑内存寻址的复杂性。将一个立即字送寄存器的MOV指令的编码格式为:B8+rw dw,其中操作码字节的值为B8+rw,表示将一个寄存器编号(0~7)与B8相加;dw为立即字操作数,低字节在低地址。(表12-21列出了操作码使用的寄存器编号。)下面例子中出现的所有数值都为十六进制。
表12-21 寄存器编号(8/16位) | |||
寄存器 | 编号 | 寄存器 | 编号 |
AX/AL | 0 | SP/AH | 4 |
CX/CL | 1 | BP/CH | 5 |
DX/DL | 2 | SI/DH | 6 |
BX/BL | 3 | DI/BH | 7 |
示例: PUSH CX 机器指令为51。编码步骤如下:
1)带一个16位寄存器操作数的PUSH指令编码为50。
2)CX的寄存器编码为1,因此1+50得到操作码为51。
示例: MOV AX,1 机器指令为B8 01 00(十六进制)。编码过程如下:
1)立即数送16位寄存器的操作码为 B8。
2)AX的寄存器编号为0,将0加上B8(参见表12-21)。
3)立即操作数(0001)按小端顺序添加到指令(01,00)。
示例: MOV BX,1234h 机器指令为BB 34 12。编码过程如下:
1)立即数送16位寄存器的操作码为B8。
2)BX 的寄存器编号为3,将3加上 B8 得到操作码BB。
3)立即操作数字节为34 12。
从实践的角度出发,建议手动汇编一些MOV立即数指令来提高能力,然后通过MASM的源列表文件中的生成代码来检查汇编结果。
完整代码测试笔记
;12.3.3.asm 12.3.3 立即数送寄存器
;将一个立即字送寄存器的MOV指令的编码格式为:B8+rw dw,其中操作码字节的值为B8+rw,
;表示将一个寄存器编号(0~7)与B8相加;dw为立即字操作数,低字节在低地址。INCLUDE Irvine32.inc.code
main PROC;push指令编码为50, DX的寄存器编码为2,因此2+50得到操作码52push dx;立即数送16位寄存器的操作码为B8,DX的寄存器编号为2,相加得到操作码BA;立即操作数22h按小端顺序添加到指令(22 00),所以机器码为 BA 22 00mov dx, 22h;立即数送16位寄存器的操作码为B8,CX的寄存器编号为1,相加得到操作码B9;立即操作数1234h按小端顺序添加到指令(34 12),所以机器码为 B9 34 12mov cx, 1234hINVOKE ExitProcess, 0
main ENDP
END main
查看反汇编:
12.3.4 寄存器模式指令
在使用寄存器操作数的指令中,ModR/M字节用一个3位的标识符来表示寄存器操作数。表12-22列出了寄存器的位编码。操作码字段的位0用于选择8位或16位寄存器1表示16位寄存器,0表示8位存器。
表12-22 Mod R/M字段标识寄存器 | |||
R/M | 寄存器 | R/M | 寄存器 |
000 | AX or Al | 100 | SP or AH |
001 | CX or CL | 101 | BP or CH |
010 | DX or DL | 110 | SI or DH |
011 | BX or BL | 111 | DI or BH |
比如,MovAx,Bx的机器码为89D8。寄存器送其他操作数的16位MOV指令的Intel编码为89r,其中/r表示操作码后面带一个Mod R/M字节。Mod R/M字节有三个字段(mod、reg和r/m)。例如,若 Mod R/M 的值为D8,则它包含如下字段:
mod | reg | r/m |
11 | 011 | 000 |
●6~7是mod字段,指定寻址模式。mod字段为11表示r/m字段包含的是一个寄存器编号。
●位3~5是reg字段,指定源操作数。在本例中,BX就是编号为011的寄存器。
●位0~2是r/m字段,指定目的操作数。本例中,AX是编号为000的寄存器。
表12-23 列出了更多使用8位和16位寄存器操作数的例子,
表 12-23 MOV 指令编码和寄存器操作数的示例 | ||||
指令 | 操作码 | mod | reg | r/m |
mov ax, dx | 8B | 11 | 000 | 010 |
mov al,dl | 8A | 11 | 000 | 010 |
mov cx,dx | 8B | 11 | 001 | 010 |
mov cl,dl | 8A | 11 | 001 | 010 |
12.3.5 处理器操作数大小前缀
现在将注意力转回到x86处理器(IA-32)的指令编码。有些指令以操作数大小前缀开始,覆盖了其修改指令的默认段属性。问题是,为什么有指令前缀?在编写8088/8086 指令集时,几乎所有 256 个可能的操作码都用于处理带有8位和 16 位操作数的指令。当 Intel 开发32位处理器时,就需要想办法发明新的操作码来处理32位操作数,而同时还要保持与之前处理器的兼容性。对于面向 16 位处理器的程序,所有使用 32位操作数的指令都添加一个前缀字节。对于面向 32 位处理器的程序,默认为 32 位操作数,因此所有使用 16 位操作数的指令添加一个前缀字节。8 位操作数不需要前缀。
示例:16位操作数 现在对表 12-23 中的 MOV 指令进行汇编,以此为例来看看在16位模式下前缀字节是如何起作用的。.286 伪指令指明编译代码的目标处理器,确保不使用32位寄存器。下面的每条MOV指令都给出了其指令编码:
.model small
.286
.stack 100h
.code
main PROCmov ax, dx ;8B C2mov al, dl ;8A C2
现在对32位处理器汇编相同的指令,使用.386 伪指令,默认操作数为 32 位。指令将包括16 位和 32 位操作数。第一条MOV指令(EAX、EDX)使用的是32 位操作数,因此不需要前缀。第二条MOV(AX、DX)指令由于使用的是16位操作数,因此需要操作数大小前缀(66):
.model small
.386
.stack 100h
.code
main PROCmov eax, edx ;8B C2mov ax, dx ;66 8B C2mov al, dl ;8A C2
12.3.6 内存模式指令
如果 Mod R/M 字节只用于标识寄存器操作数,那么 Intel 指令编码就会相对简单。实际上,Intel 汇编语言有着各种各样的内存寻址模式,这就使得Mod R/M 字节编码相当复杂。(指令集的复杂性是RISC设计支持者常见的批评理由。
Mod R/M 字节正好可以指定256 个不同组合的操作数。表 12-24 列出了Mod 00 时的Mod R/M字节(十六进制)。(完整的表格参见《Intel 64and IA-32 Architectures SoftwareDeveloper's Manual》,卷2A。)Mod R/M 字节编码的作用如下:Mod 列中的两位指定寻址模式的集合。比如,Mod 00 有 8 种可能的 R/M 数值(000b~111b),有效地址列给出了这些数值标识的操作数类型。
假设想要编码MOV AX,[SI],Mod位为00b,R/M位为100b。从表12-19可知 AX的寄存器编号为000b,因此完整的Mod R/M 字节为00 000 100b 或04h:
mod | reg | r/m |
00 | 000 | 100 |
十六进制字节 04 在表12-24 的AX列第5 行。
MOV [SI],AL 的Mod R/M 字节还是一样的(04h),因为寄存器AL 的编号也是000。现在对指令MOV [SI],AL进行编码。8 位寄存器的传送操作码为88。Mod R/M 字节为04h,则机器码为88 04。
MOV 指令示例
表12-25列出了8位和16位MOV指令所有的指令格式和操作码。表 12-26 和表 12-27给出了表 12-25 中缩写符号的补充信息。手动汇编 MOV指令时可以用这些表作为参考。(更多细节请参阅Intel 手册。)
表12-28列出了更多的MOV指令,这些指令能手动汇编,且可以与表中的机器代码比较。假设myWord的起始地址偏移量为0102h
12.3.7 本节回顾
1.写出下列 MOV 指令的操作码:
.data
myByte BYTE ?
myWord WORD ?
.codemov ax, @datamov ds, ax ;a. 8Emov ax, bx ;b. 8Bmov bl, al ;c. 8Amov al, [si] ;d. 8Amov myByte, al ;e. A2mov myWord, ax ;f. A3
2.写出下列 MOV 指令的 Mod R/M 字节:
.data
array WORD 5 DUP(?)
.codemov ax, @datamov ds, ax ;a. D8mov dl, bl ;b. D3mov bl, [di] ;c. 1Dmov ax, [si+2] ;d. 44mov ax, array[si] ;e. 84mov array[di], ax ;f. 85
12.4 本章小结
二进制浮点数由三部分组成:符号、有效数字和阶码。Intel处理器使用了三种浮点数一进制存储格式,这些格式都出自由IEEE组织制定的标准754-1985:二进制浮点数运算:
●32位单精度数值包含1位符号、8位阶码,以及23位有效数字的小数部分。
●64位双精度数值包含1位符号、11位阶码,以及52位有效数字的小数部分。
●80 位扩展双精度数值包含1位符号、16 位阶码,以及 63 位有效数字的小数部分。
若符号位为 1,则数值为负数;若该位为 0,则数值为正数。
浮点数的有效数字由小数点左右两边的十进制数字构成。
并非所有处于0到1之间的实数都可以在计算机内表示为浮点数,其原因是有效位的个数是有限的。
规格化有限数是指,能够编码为0到无穷之间的规格化实数的所有非零有限数值。正无穷(+∞)代表最大正实数,负无穷(-∞)代表最大负实数。NaN 为位模式,不表示有效浮点数。
Intel 8086 处理器被设计为只处理整数运算,因此 Intel 提供了独立的 8087 浮点数协处理器(floating-pointcoprocessor)芯片与8086一起置于计算机的主板上。随着Intel486的出现,浮点操作被整合到主 CPU 内,称为浮点单元(FPU)。
FPU 有 8 个相互独立的可寻址的 80 位寄存器,分别命名为RO~R7,它们构成一个寄存器堆栈。在计算时,浮点操作数以扩展实数的形式保存在FPU 堆栈中。内存操作数也可以用于计算。FPU 在把算术运算操作的结果保存到内存时,会把结果转换为下述格式之一:整数、长整数、单精度数、双精度数或者 BCD 码。
Intel 浮点指令助记符用字母F开始,以区别于 CPU 指令。指令的第二个字母(通常为B或I)表示如何解释内存操作数:B表示为操作数为二进制编码的十进制数(BCD 码),I表示操作数为二进制整数。如果没有指定,那么内存操作数就假设为实数格式。
Intel 8086 是第一个使用复杂指令集计算机(CISC)设计的处理器。其庞大的指令集包含了各种各样的内存寻址、移位、算术运算、数据传送和逻辑操作。
指令编码是指把汇编语言指令及其操作数转换为机器码。指令解码是指将机器码指令转换为汇编语言指令及操作数。
x86机器指令格式包含一个可选的前缀字节、一个操作码字段、一个可选的Mod R/M字节、若干可选的立即数字节,以及若干可选的内存偏移量字节。具备全部这些字段的指令很少。前缀字节覆盖了目标处理器默认的操作数大小。操作码字段是指令独一无二的操作编码。Mod R/M 字段指定了寻址模式和操作数。在使用寄存器操作数的指令中,Mod R/M 字节用一个3位标识符来表示每个寄存器操作数。