80x86程序员可以从大量编程开发工具中挑选自己喜欢的工具使用,但这种丰富性有一个小缺点:语法可能存在不兼容性。对同样一个程序,80x86家族上的不同编译器和调试器会产生不同的汇编语言清单。这是因为那些工具为不同的汇编器发出代码。例如,微软Visual C++软件包产生的汇编语言代码兼容于其MASM,GNU的编译器套件(GCC)产生与Gas兼容的源代码—Gas是自由软件基金会(Free Software Foundation)的GNU汇编器。除了由编译器发出的代码,我们还会发现大批汇编语言编程示例是由FASM、NASM、GoAsm、HLA(High-Level Assembly)等汇编器写成的。
只用某种汇编器的语法贯穿本书自然不错,但由于我们的方法并不特定于某个编译器,必须考虑到几种常见汇编器的语法。本书通常用HLA给出不特定编译器的示例。因此,本章将讨论HLA和另外两种汇编器,即MASM和Gas的语法。幸运的是,如果我们掌握了一种汇编器的语法,学习其他汇编器的语法将会非常轻松。
Intel的CPU一般归类于冯·诺依曼(Von Neumann)结构。冯·诺依曼计算机系统主要包含三大部分:中央处理单元(CPU)、内存和输入/输出(I/O)设备。这3个组件通过系统总线(包括地址总线、数据总线和控制总线)相连。图3-1给出了其相互关系。
图3-1 冯·诺依曼计算机系统模块图
CPU将地址值放入地址总线,以此选择内存单元位置或I/O设备端口位置,从而与内存和外设通信。这些内存单元或端口位置在系统中占有唯一的二进制地址值。然后,CPU、内存和I/O设备通过数据总线互相传递数据。控制总线里的信号线控制着数据进出内存或I/O设备的方向。
寄存器组是CPU内部最突出的特性。几乎所有80x86 CPU操作都涉及至少一个寄存器。例如,要将两个变量的值加起来,将结果放入第3个变量,必须将其中一个变量放入寄存器,对其加上第2个操作数,然后将结果送入目标变量。寄存器几乎是每个操作的中介。因此在80x86汇编语言程序中,寄存器是必不可少的。
80x86 CPU的寄存器可以分成四类:通用寄存器、特殊目的寄存器(应用可访问)、段寄存器和特殊目的核心模式寄存器。由于段寄存器在Windows、BSD、MacOS和Linux等现代操作系统中不常用,而特殊目的核心模式寄存器用来编写操作系统、调试器和其他系统级工具,我们不再考虑段寄存器和特殊目的核心模式寄存器。
Intel的32位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位寄存器重叠,16位寄存器又与8位寄存器重叠。图3-2给出了其相互关系。
因此实际修改某个寄存器内容时,可能没有特别指明就改动了3个寄存器的值。例如,修改寄存器EAX也许会改变寄存器AL、AH和AX的值。我们将会经常看到编译器所产生的代码用到80x86的这种特性。举个例子,编译器会清除(置0)寄存器EAX的所有位,然后将AL设为1或0,以便得到一个32位的true(1)或false(0)值。有的机器指令只操纵AL寄存器,而程序也许需要以EAX返回这些指令的结果。利用寄存器重叠的优点,编译器产生的代码就能够通过操纵AL的指令而返回整个EAX值。
图3-2 Intel 80x86 CPU的通用寄存器
虽然Intel将这些寄存器称作通用寄存器,但不能因而以为它们可用于任何目的。比如,寄存器SP/ESP有着专门用途,切勿挪作他用,因为它是栈指针。类似地,寄存器BP/EBP也是专用的,无法当作通用寄存器使用。所有80x86寄存器都有各自的特殊意图,仅可在特定环境下使用。在讨论使用这些寄存器的机器指令时,我们会考虑这些特殊用法的,请参看在线资源。
80x86 CPU的当代版本(即 x86-64 CPU )为32位寄存器提供了两个重要的扩展:一套64位寄存器及二分之一套8个寄存器(64位、32位、16位、8位)。主要的64位寄存器有下列名字:RAX、RBX、RCX、RDX、RSI、RDI、RBP和RSP。
这些64位寄存器与32位名字以“E”打头的寄存器重叠。即32位寄存器占据了64位寄存器低32位。例如,EAX是RAX的低32位。类似地,AX是RAX的低16位;AL是RAX的低8位。
除了提供已有80x86 32位寄存器的64位变种,x86-64 CPU还另外增加了8个64/32/16/8位寄存器:R15、R14、R13、R12、R11、R10、R9和R8。
我们可以这么引用上述寄存器的低32位:R15d、R14d、R13d、R12d、R11d、R10d、R9d和R8d;这么引用上述寄存器的低16位:R15w、R14w、R13w、R12w、R11w、R10w、R9w和R8w;最后,这么引用上述寄存器的低字节:R15b、R14b、R13b、R12b、R11b、R10b、R9b和R8b。
32位EFLAGS寄存器将许多单一比特位的布尔值(true/false)集合在一起,这些单一比特位又被称为“标志位”。这些比特位大部分要么为操作系统的核心模式函数保留,要么与应用程序员没有太大关系。但是,应用程序员要用汇编语言代码读/写其中的8位:溢出标志位、方向标志位、中断标志位 、符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位。图3-3展示了EFLAGS寄存器内这些标志位的布局。
应用程序员可用的这8个标志位中,4个标志位具有特别价值:溢出标志位、进位标志位、符号标志位和零标志位。我们将这4个标志位称为条件码(condition code)。每个标志位都有一个状态—要么是设置(1),要么是清除(0),可以用来检验上次运算的结果。例如,在比较两个值后,条件码标志可告诉我们,其中一个值小于、等于,还是大于另一个值。
图3-3 EFLAGS寄存器低16位的布局
图3-3 EFLAGS寄存器低16位的布局(续)
x86-64的64位RFLAGS寄存器将b32到b63之间的比特位保留。EFLAGS的高16位通常只对操作系统代码有用。
我们阅读编译器输出时,由于RFLAGS寄存器并不包含有价值的信息,本书将只会按EFLAGS参考x86和x86-64的标志位。即便是在64位版本的CPU上,也是如此。