更好的阅读体验,请点击 YinKai 's Blog。
下面来介绍一个汇编语言中三种基本寻址方式:
寄存器寻址模式,其中操作数直接存储在寄存器中,而不涉及内存。这种寻址模式在处理数据时提供了高效的速度,因为它是直接从寄存器中读取或向寄存器中写入数据,而无需涉及到主存储器。
在此模式下,根据指令的不同,寄存器可能是第一个操作数,也有可能是第二个操作数,或者两个操作数都是,如下:
MOV DX, TAX_RATE
MOV COUNT, CX
MOV EAX, EBX
立即寻址模式,其中一个操作数是常量或者表达式,而不是从内存中获取的。
我们可以通过这种方式定义变量、更改变量值、赋值等操作,例如:
BYTE_VALUE DB 150
ADD BYTE_VALUE, 65
MOV AX, 45H
直接内存寻址用于操作内存中的数据。在该模式下,偏移值直接指定为指令的一部分,通常由变量名指示。这种寻址方式涉及两个操作:定位内存位置和执行操作。
举例如下:
ADD BYTE_VALUE DL ; 将寄存器 DL 中的值加到内存位置 BYTE_VALUE 的字节值上
MOV BX, WORD_VALUE ; 将内存中的操作数直接赋值给 BX 寄存器
上述两种情况下,汇编器会维护一个符号表,其中存储了程序中所使用的所有变量的偏移值,这些偏移值用于在运行时计算实际的内存地址。这种方式使用了一种简单而直接的方法来引用内存中的数据,但相对寄存器寻址或间接寻址来说,它可能导致访问效率稍低。
直接偏移寻址是一种在汇编语言中用于访问数据表的寻址模式。通过使用算术运算符,你直接可以直接计算或指定相对于数据表起始地址的偏移量,从而访问表格中的特定数据。
我们先定义以下数据表,以供我们后续的操作:
BYTE_TABLE DB 14, 15, 22, 23
然后我们可以通过索引和偏移量的方式操作数据表中的数据:
MOV CL, BYTE_TABLE[2] ; 元素索引方式
MOV CL, BYTE_TABLE + 2 ; 偏移量方式
间接内存寻址是一种利用计算机的段:偏移寻址能力的寻址模式。通常,基址寄存器(例如 EBX、EBP,或简写为 BX、BP)和索引寄存器(DI、SI)被包含在方括号内,用于存储器引用,从而实现对内存中数据的访问。这种寻址模式通常用于处理包含多个元素的变量,比如数组。在数组的情况下,数组的起始地址通常存储在基址寄存器中。
通过下面的代码,演示一下如何通过间接内存寻址访问数组的不同元素:
MY_TABLE TIMES 10 DW 0 ; 分配了10个字,每个字2字节并初始化为0
MOV EBX, [MY_TABLE] ; 将 MY_TABLE 的有效地址存储到 EBX 寄存器中
MOV [EBX], 110 ; 将值 110 存储到 MY_TABLE 的第一个有效地址
ADD EBX, 2 ; EBX = EBX + 2
MOV [EBX], 123 ; 将值 123 存储到 MY_TABLE 的第二个元素
:::warning
用 [] 和 不用 [] 的区别?
对于 MOV [EBX], 110
和 MOV EBX, 110
来说:
MOV [EBX], 110
:是一条间接寻址指令,它将立即数 110 存储到 EBX 寄存器中存储的内存地址指向的位置。MOV EBX, 110
:这是一条直接寻址指令,它将立即数 100 直接加载到 EBX 寄存器中,此时 EBX 中存储的是一个数,而不是内存地址。:::
MOV 指令是 x86 汇编语言中用于将数据从一个存储空间移动到另一个存储空间的指令,它通常需要两个操作数,语法如下:
MOV destination, source
MOV 指令可能有以下五种形式,register(寄存器)、immediate(立即数)、memory(内存):
MOV register, register
MOV register, immediate
MOV memory, immediate
MOV register, memory
MOV memory, register
:::warning
需要注意的是:
我们上面写的代码其实是存在一些问题的:
MY_TABLE TIMES 10 DW 0 ; 分配了10个字,每个字2字节并初始化为0
MOV EBX, [MY_TABLE] ; 将 MY_TABLE 的有效地址存储到 EBX 寄存器中
MOV [EBX], 110 ; 将值 110 存储到 MY_TABLE 的第一个有效地址
由于 x86 架构中内存访问是按字节寻址的,MOV [EBX], 110
这条指令可能会被解释为存储到 MY_TABLE 的第一个字节;但可能程序员的目的是存储一个整数值 110 到 MY_TABLE,并且 MY_TABLE 中的每个元素是字,那就会有歧义。
因此,我们需要使用类型说明符来明确指令操作的数据类型和占用的字节数,于是可以像下面这样写:
MOV [EBX], WORD 110 ; 将一个字(两个字节)的值 110 存储到 MY_TABLE[0] 中
常见的类型说明符如下:
类型说明符 | 寻址字节数 |
---|---|
BYTE | 1 |
WORD | 2 |
DWORD | 4 |
QWORD | 8 |
TBYTE | 10 |
:::
在汇编语言中,变量的定义和数据的存储通常涉及到不同的指令和规则。
NASM 提供了不同的 define 指令,用于为变量分配存储空间。这些指令用于在数据段中保留和初始化一个或多个字节,常见的有:
指令 | 用途 | 存储空间 |
---|---|---|
DB | 定义 Byte | 分配1个字节 |
DW | 定义 Word | 分配2个字节 |
DD | 定义 Doubleword | 分配4个字节 |
DQ | 定义 Quadword | 分配8个字节 |
DT | 定义十个字节 | 分配10个字节 |
使用示例如下:
choice DB 'y'
number DW 12345
neg_number DW -12345
big_number DQ 123456789
real_number1 DD 1.234
real_number2 DQ 123.456
:::warning
字符的每个字节都以其十六进制的 ASCII 值存储:
每个字符都有一个对应的 ASCII 值,它是一个唯一的数值表示。例如,字母 ‘A’ 的 ASCII 值是 65(十六进制为 41),而字母 ‘B’ 的 ASCII 值是 66(十六进制为 42)。
当你在程序中定义一个字符变量,它的每个字节将被存储为对应字符的 ASCII 值的十六进制表示。
每个十进制值都会自动转换为其 16 位二进制等效值并存储为十六进制数:
当你在程序中定义一个十进制值,汇编器会将其自动转换为其 16 位的二进制等效值,并以十六进制形式存储。
例如,十进制值 10 会被转换为二进制值 1010,然后以十六进制形式存储为 “A”。
处理器使用小尾数字节排序:
处理器采用小尾数(Little Endian)字节排序,这意味着较低有效字节(最低位字节)存储在内存中的较低地址处,而较高有效字节(最高位字节)存储在内存中的较高地址处。
例如,对于十六进制值 0x12345678,在内存中的存储顺序是:78 56 34 12。
负数将转换为其 2 的补码表示形式:
在计算机中,负数通常以 2 的补码形式表示。这种表示方式使得在计算中可以统一处理加法和减法,而不需要额外的逻辑。
2 的补码表示形式是通过将正数的二进制表示取反然后加 1 得到的。例如,-5 的二进制表示是将 5 的二进制表示(0000 0101)取反得到(1111 1010),然后加 1 得到(1111 1011)。
短浮点数和长浮点数分别使用 32 位或 64 位表示:
浮点数表示采用 IEEE 754 标准,其中短浮点数(float)通常使用 32 位表示,而长浮点数(double)通常使用 64 位表示。
32 位浮点数包括符号位、8 位指数和 23 位尾数。64 位浮点数包括符号位、11 位指数和 52 位尾数。
:::
下面程序演示了 define 指令的使用:
section .text
global _start
section .data
choice db 'y', 0xa
len equ $ - choice
_start:
mov edx, len
mov ecx, choice
mov ebx, 1
mov eax, 4
int 80h
mov eax, 1
int 80h
编译运行后输出如下:
y
在程序中,有时我们需要为一些数据保留一些存储空间,但不需要为它们初始化,而是在后续程序运行过程中被赋予实际值。
这个时候,我们就需要用到汇编语言中的保留指令,这些指令用于在内存中分配指定大小的空间,但不对其中的数据进行初始化。
常见的保留指令有:
指令 | 用途 |
---|---|
RESB | 保留一个 Byte(一个字节) |
RESW | 保留一个 Word(两个字节) |
RESD | 保留一个 Doubleword(四个字节) |
RESQ | 保留一个 Quadword(八个字节) |
REST | 保留十个字节空间(十个字节) |
一个程序可以定义多个数据定义的语句,例如:
choice DB 'y'
number DW 123
bigbumber DQ 123456789
这样定义的语句,编辑器会为这些变量分配连续的内存。
times 指令允许对同一值进行多次初始化,这样在定义数组和表示非常有用。可以使用如下语句,创建名为 stars、数据元素类型为DW 的数组,包含 9 个元素,每个初始化为 0.
stars TIMES 9 DW 0 ;
我们重温一下上面输出 9 个 * 的汇编程序:
section .text
global _start
section .data
stars times 9 db '*'
_start:
mov edx, 9
mov ecx, stars
mov ebx, 1
mov eax, 4
int 0x80
mov eax, 1
int 0x80
同样编译运行后,会输出:
*********