汇编语言
x86寄存器与内存寻址模式

本指南描述了32位x86汇编语言编程的几个基础知识,主要介绍寄存器和内存寻址。 有几种不同的汇编语言可用于生成x86机器代码,本文使用标准的Intel语法来编写x86汇编代码。

寄存器

如图1所示,现代(即386及更高版本)x86处理器有8个32位的通用寄存器。

寄存器的名称大多是基于历史的命名而来。 例如,EAX过去被称为累加器,因为它被大量用于算术运算;而ECX被称为计数器,因为它被用于保存循环索引。

尽管大多数寄存器在现代指令集中已失去了其特殊用途,但是按照使用惯例,有两个寄存器仍然被保留用于特殊用途 — 堆栈指针(ESP)和基址指针(EBP)。

对于EAX、EBX、ECX和EDX四个寄存器,支持以子寄存器的方式使用。 例如,EAX的2个最低有效字节,可作为一个16位的寄存器AX来使用。AX的最低有效字节,可以用作一个8位的寄存器AL;而AX的最高有效字节,可以用作一个8位的寄存器AH。这些名称指的都是相同的物理寄存器。

当将两个字节的数据存入DX时,这个更新操作会同时影响DH、DL和EDX的值。

这些子寄存器主要是从过去的16位版本指令集保留下来的。 但是,当处理小于32位的数据时(例如处理1个字节的ASCII字符),它们有时却很方便。

使用汇编语言引用寄存器时,名称不区分大小写。例如,名称EAX和eax指的是同一个寄存器。

内存和寻址模式

声明静态数据区

在x86汇编中,使用特殊的汇编指令来声明静态数据区(类似于全局变量)。在声明数据的前面,需要加上 .DATA 指令。 同时,可以使用DB、DW和DD来分别声明大小为1个字节、2个字节和4个字节的数据位置。

声明的位置可以用名称标记,以便后续引用该位置 — 这类似于使用名称来定义一个变量,但是遵循的是一些较低级别的规则。 例如,按照顺序声明的数据位置将位于内存中彼此相邻的位置。

示例:

.DATA		
var   DB 64         ; 声明一个字节,标记为位置var(用名称var来标记、引用该位置),该字节的值为64。
var2	DB ?           ; 声明一个未初始化的字节,标记为位置var2。
        DB 10         ; 声明一个没有标记名称的字节,该字节的值为10。其位置为 var2 + 1
X      DW ?          ; 声明一个2个字节的未初始化的值,标记为位置X。
Y      DD 30000  ; 声明一个4个字节的值,标记为位置Y,初始化值为30000。

在高级语言中,一个数组可以是多维的,并且可以通过索引进行访问。 与高级语言不同,在x86汇编语言中的数组,只是在内存中连续分布的多个内存单元。 要声明一个数组,只需要顺序的列出多个值,如下面的第一个示例所示。

另外两个用于声明数组的常用方法,分别是使用DUP指令和字符串字面量。

通过DUP指令,来通知汇编程序,按照给定的次数来复制指定的表达式。 例如:

4 DUP(2)

等价于

2, 2, 2, 2

一些示例:

Z           DD 1, 2, 3           ; 声明三个4字节的值,分别初始化为1、2和3。则位置Z的值为1,位置Z+4的值为2,位置Z+8的值为3
bytes     DB 10 DUP(?)     ; 从位置bytes标记的地址开始,声明10个未初始化的字节
arr         DD 100 DUP(0)  ; 从位置arr标记的地址开始,声明100个四字节的字,值都初始化为0
str         DB 'hello',0         ; 从位置str标记的地址开始,声明6个字节,初始化为字符串hello的ASCII字符值,和一个null(0)字节

内存寻址

现代的x86兼容处理器能够寻址多达2的32次方个字节的内存:内存地址为32位宽。 在上面的示例中,我们使用标记来引用内存区域,这些标记实际上会被汇编器替换为32位的数值,用来指定内存中的地址。

除了支持通过标记(即常量值)引用内存区域外,x86还提供了一种灵活的方案来计算和引用内存地址:最多可以将两个32位的寄存器和一个32位的带符号常量加在一起来计算内存地址。 其中的一个寄存器,可以选择预乘以2、4或8。

寻址模式可以和许多x86指令一起使用。 下面我们举一些例子,使用mov指令在寄存器和内存之间移动数据。 该指令有两个操作数:第一个是目标操作数,第二个是源操作数。

mov指令使用地址计算的一些示例:

mov eax, [ebx]              ; 将寄存器EBX的值对应的内存地址的4个字节,传入寄存器EAX
mov [var], ebx               ; 将寄存器EBX的值,传入内存地址 var 对应的4个字节(注意,var是一个32位常量值)
mov eax, [esi-4]            ; 将内存地址 ESI-4 的4个字节,传入寄存器EAX
mov [esi+eax], cl           ; 将寄存器CL的值,传入内存地址为 ESI+EAX 对应的1个字节
mov edx, [esi+4*ebx]    ; 将内存地址 ESI+4*EBX 的4个字节的数据,传入寄存器EDX

一些无效的地址计算示例:

mov eax, [ebx-ecx]           ; 寄存器之间只能相加不能相减
mov [eax+esi+edi], ebx    ; 地址计算最多只能使用2个寄存器

尺寸操作符

一般来说,给定内存地址的数据项的预期大小,可以从汇编代码指令中推断出来。 例如,在上面的所有指令中,内存区域的大小都可以从寄存器操作数的大小推断出来。

当我们加载一个32位寄存器时,汇编程序可以推断出我们所引用的内存区域是4个字节宽。 当我们将单字节寄存器的值存储到内存中时,汇编程序可以推断出我们希望引用的地址是指向内存中的一个字节。

然而,在某些情况下,被引用的内存区域的大小是不明确的。

例如有这样一条指令:

mov [ebx], 2

这条指令是否表示应该将2传入内存地址 EBX 对应的一个字节中? 也许应该表示将32位的整数2,传入内存地址 EBX 开始的4个字节中。

由于这两种情况都是有可能的,因此必须在指令中显式的告诉汇编程序哪一个是正确的。 使用BYTE PTR、WORD PTR 和 DWORD PTR操作符,可以显式的指明内存单元的大小,这三个操作符分别表示1个字节、2个字节和4个字节的内存单元。

例如:

mov BYTE PTR [ebx], 2           ; 将2传入内存地址 EBX 的一个字节中
mov WORD PTR [ebx], 2        ; 将16位的整数2,传入内存地址 EBX 开始的2个字节中
mov DWORD PTR [ebx], 2      ; 将32位的整数2,传入内存地址 EBX 开始的4个字节中

参考

英文原文:http://www.cs.virginia.edu/~evans/cs216/guides/x86.html

Intel's Pentium Manuals:https://software.intel.com/content/www/us/en/develop/articles/intel-sdm.html (opens in a new tab)

Intel x86 Instruction Set Reference:https://www.felixcloutier.com/x86/ (opens in a new tab)

Guide to Using Assembly in Visual Studio .NET:http://www.cs.virginia.edu/~evans/cs216/guides/vsasm.html