64位汇编语言的编程艺术
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.7 英特尔x86-64 CPU系列简介

到目前为止,读者已经学习了一个可以编译并运行的MASM程序。然而,该程序没有执行任何操作,只是将控制权返回到Windows。在进一步学习真正的汇编语言之前,必须先了解硬件方面的知识。读者必须了解英特尔x86-64 CPU系列的基本结构,否则将无法理解机器指令。

英特尔CPU系列通常被归类为冯·诺依曼体系结构的计算机。冯·诺依曼计算机系统包含三个主要的组成部分:中央处理器(CPU)、内存(memory)和输入/输出(I/O)设备。这三个组件通过系统总线(包括地址总线、数据总线和控制总线)互连。图1-1中的方框图显示了这三个组件之间的关系。

CPU通过在地址总线上放置一个数值,来选择一个内存位置或者I/O设备端口位置(每个位置都有一个唯一的数字地址),从而与内存或I/O设备通信。然后,CPU、内存和I/O设备这三个组件通过将数据放在数据总线上,实现数据传输。控制总线包含确定数据传输方向(从内存到I/O设备或从I/O设备到内存)的信号。

图1-1 冯·诺依曼计算机系统的方框图

在CPU中,寄存器(register)用于操作数据。x86-64 CPU的寄存器可分为四种类型:通用寄存器(general-purpose register)、专用应用程序访问寄存器(special-purpose application-accessible register)、段寄存器和专用内核模式寄存器(special-purpose kernel-mode register)。由于段寄存器在现代64位操作系统(例如Windows)中使用不多,因此本书中将不加讨论。专用内核模式寄存器用于编写操作系统、调试器和其他系统级的工具。这种软件构造远远超出了本书的讨论范围。

x86-64(英特尔系列)CPU提供多个通用寄存器供应用程序使用。通用寄存器包括如下几类。

●16个64位寄存器,对应的名称如下:RAX、RBX、RCX、RDX、RSI、RDI、RBP、RSP、R8、R9、R10、R11、R12、R13、R14和R15。

●16个32位寄存器,对应的名称如下:EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP、R8D、R9D、R10D、R11D、R12D、R13D、R14D和R15D。

●16个16位寄存器,对应的名称如下:AX、BX、CX、DX、SI、DI、BP、SP、R8W、R9W、R10W、R11W、R12W、R13W、R14W和R15W。

●20个8位寄存器,对应的名称如下:AL、AH、BL、BH、CL、CH、DL、DH、DIL、SIL、BPL、SPL、R8B、R9B、R10B、R11B、R12B、R13B、R14B和R15B。

遗憾的是,以上列举的68个寄存器都不是独立的寄存器。实际上,x86-64的64位寄存器覆盖在32位寄存器上,32位寄存器覆盖在16位寄存器上,16位寄存器覆盖在8位寄存器上。表1-1显示了这些寄存器之间的关系。

1-1 x86-64上的通用寄存器

通用寄存器不是独立的寄存器,使得修改其中1个寄存器可能引发对其他寄存器的修改。例如,修改EAX寄存器的内容可能也会正好修改AL、AH、AX和RAX寄存器的内容。这一连锁反应现象需要引起足够的重视。在汇编语言初级程序员编写的程序中,一个常见的错误是寄存器值的损坏,这是程序员没有完全理解表1-1中所示关系的后果。

除通用寄存器外,x86-64还提供专用寄存器,包括在x87浮点单元(floating-point unit,FPU)中实现的8个浮点寄存器(floating-point register)。英特尔将这些寄存器命名为ST(0)到ST(7)。与通用寄存器不同,应用程序不能直接访问这些寄存器,它会将浮点寄存器文件视为一个可以包括8个项的栈,并只访问栈最上面的一个或两个项(有关更多的详细信息,可以参阅6.5节的相关内容)。

每个浮点寄存器的宽度为80位,包含一个扩展精度实数值(以下简称扩展精度)。尽管多年来英特尔在x86-64 CPU中增加了其他浮点寄存器,但FPU寄存器在代码中仍然很常用,因为这些寄存器支持这种80位的浮点格式。

20世纪90年代,英特尔推出了MMX寄存器集和指令,以支持单指令多数据(Single Instruction,Multiple Data,SIMD)操作。MMX寄存器集由8个64位的寄存器组成,覆盖FPU上的ST(0)至ST(7)寄存器。英特尔选择覆盖FPU寄存器,是因为这种方式使得MMX寄存器能够立即与多任务操作系统(例如Windows)兼容,而无须针对这些操作系统进行任何代码更改。遗憾的是,这种选择意味着应用程序不能同时使用FPU指令和MMX指令。

英特尔在x86-64的后续版本中通过添加XMM寄存器集解决了以上问题,因此用户很少看到使用MMX寄存器和指令集的现代应用程序。如果用户真想使用MMX寄存器和指令集,也不是不可以,但还是建议使用XMM寄存器(和指令集),同时将MMX寄存器保持为FPU模式。

为了打破MMX寄存器和FPU寄存器之间冲突带来的限制,AMD/英特尔增加了16个128位的XMM寄存器(XMM0到XMM15)和SSE/SSE2指令集。每个寄存器可以配置为4个32位的浮点寄存器,2个64位的双精度浮点寄存器,或者16个8位、8个16位、4个32位、2个64位或1个128位的整数寄存器。在x86-64 CPU系列的后续版本中,AMD/英特尔将寄存器的大小增加了一倍,每个寄存器的大小变为256位(并将这些寄存器重命名为YMM0到YMM15),以支持8个32位的浮点数值或者4个64位的双精度浮点数值(整数运算仍限制为128位)。

RFLAGS(或FLAGS)寄存器是一个64位寄存器,它封装了多个1位的布尔值(真/假)[1]。RFLAGS寄存器中的大多数位或者是为内核模式(操作系统)函数保留的位,或者是应用程序程序员不关心的位。编写汇编语言应用程序的程序员特别关注其中的8个位(或称为标志):溢出(overflow)、方向(direction)、中断(interrupt)[2]、符号(sign)、零(zero)、辅助进位(auxiliary carry)、奇偶校验(parity)以及进位(carry)标志。图1-2显示了RFLAGS寄存器低16位中各个标志位的布局。

图1-2 RFLAGS寄存器的布局(位于RFLAGS寄存器的低16位)

在图1-2中,4个标志特别重要,分别是溢出、进位、符号和零标志,统称为条件码(condition code)[3]。这些标志的状态允许用户测试先前计算的结果。例如,比较两个值后,条件码标志将告知用户第一个值是否小于、等于或大于第二个值。

对于那些刚刚学习汇编语言的人而言,他们惊讶于一个重要事实,即x86-64 CPU上几乎所有的计算都涉及寄存器。例如,为了将两个变量相加并将总和存储到第三个变量中,就必须将其中一个变量加载到寄存器中,将第二个操作数与寄存器中的数值相加,然后将寄存器中的值存储到目标变量中。在几乎所有的计算中,都需要以寄存器作为中介。

用户还应该知道,尽管寄存器被称为通用寄存器,但用户不能将任何寄存器用于任何目的。所有x86-64寄存器都有自己的特殊用途,从而限制了寄存器在某些上下文中的使用。例如,RSP寄存器有一个非常特殊的用途,可以有效地防止用户将其用于任何其他用途(该寄存器是堆栈指针)。同样,RBP寄存器也有一个特殊用途,限制了其作为通用寄存器的作用。目前,用户应该避免使用RSP和RBP寄存器进行一般的计算任务;另外,请记住,其余寄存器在程序中不能完全互换。