汇编语言基础(X86)
x86汇编语言是一种低级语言,用于编写与特定处理器架构(如Intel x86架构)兼容的程序。
- 指令(Instructions): 指令是CPU执行的操作,例如
mov
、add
、sub
等。 - 寄存器(Registers): 寄存器是CPU内部的小型存储设备,用于临时存储数据。在x86架构中,常见的寄存器包括
eax
、ebx
、ecx
、edx
等。 - 操作数(Operands): 操作数可以是立即数(直接给出的数值)、寄存器或内存地址。
- 地址模式(Addressing Modes): x86汇编支持多种地址模式,用于指定操作数的位置,如直接寻址、间接寻址、基址加偏移寻址等。
- 伪指令(Directives): 伪指令提供了组织程序的方法,例如定义数据段(
.data
)、代码段(.text
)等。 - 标签(Labels): 标签用于标记程序中的位置,通常用于跳转和循环。
- 注释(Comments): 在汇编语言中,注释通常用分号
;
开始,用于解释代码的功能。 - Intel与AT&T语法: x86汇编语言有两种主要的语法风格,Intel语法和AT&T语法。它们在寄存器前缀、立即数表示和操作数顺序上有所不同。
1. 寄存器
在x86汇编中,寄存器是用来存储数据和执行算术逻辑运算的。常见的寄存器包括:
- 通用寄存器:
eax
、ebx
、ecx
、edx
、esi
、edi
、ebp
、esp
。 - 指令指针寄存器:
eip
。 - 段寄存器:
cs
、ds
、es
、fs
、gs
、ss
。 - 标志寄存器:
eflags
。
2. 指令
x86汇编语言使用一系列指令来执行各种操作,包括数据传输、算术逻辑运算、跳转、条件分支等。一些常见的指令包括:
- 数据传输指令:
mov
(移动数据)、push
(将数据入栈)、pop
(将数据出栈)。 - 算术逻辑指令:
add
(加法)、sub
(减法)、mul
(乘法)、div
(除法)、and
(与运算)、or
(或运算)等。 - 控制流指令:
jmp
(无条件跳转)、je
(相等时跳转)、jne
(不相等时跳转)、call
(调用函数)、ret
(返回函数)等。
3. 段和偏移地址
在x86架构中,内存地址由段地址和偏移地址组成。在汇编语言中,可以使用段地址和偏移地址来引用内存中的数据或指令。
4. 栈
栈是一种后进先出(LIFO)的数据结构,在汇编语言中经常用于保存临时数据和函数调用的返回地址。栈的操作包括入栈(push
)和出栈(pop
)操作。
5. 程序结构
汇编程序通常由数据段(.data
)、代码段(.text
)和堆栈段(.stack
)组成。数据段用于声明程序中使用的变量,代码段包含程序的指令,堆栈段用于管理函数调用和临时数据的存储。
指令:
- 数据传送指令:
mov
: 传送数据,例如mov eax, ebx
将ebx
寄存器的值传送到eax
寄存器。push
: 将数据压入栈中,例如push eax
。pop
: 将数据从栈中弹出,例如pop eax
。
- 算术指令:
add
: 加法,例如add eax, 1
将eax
寄存器的值加1。sub
: 减法,例如sub eax, 1
将eax
寄存器的值减1。inc
: 自增,例如inc eax
将eax
寄存器的值增加1。dec
: 自减,例如dec eax
将eax
寄存器的值减少1。
- 逻辑指令:
and
: 逻辑与,例如and eax, ebx
。or
: 逻辑或,例如or eax, ebx
。xor
: 逻辑异或,例如xor eax, ebx
。not
: 逻辑非,例如not eax
。
- 控制流指令:
jmp
: 无条件跳转,例如jmp label
跳转到标签label
。je
: 如果相等则跳转,例如je label
。jne
: 如果不相等则跳转,例如jne label
。call
: 调用子程序,例如call function
。ret
: 从子程序返回。
- 移位指令:
shl
: 逻辑左移,例如shl eax, 1
将eax
寄存器的值左移1位。shr
: 逻辑右移,例如shr eax, 1
将eax
寄存器的值右移1位。
[这些指令是汇编语言编程中的基础,通过组合这些指令,可以实现更复杂的操作和算法。学习这些指令的使用方法和它们如何影响CPU的寄存器和内存是理解和编写汇编程序的关键。
寻址内存中的数据
处理器控制指令执行的过程称为取指-解码-执行周期或执行周期。 它由三个连续的步骤组成 −
- 从内存中获取指令
- 解码或识别指令
- 执行指令
处理器一次可以访问一个或多个字节的内存。 让我们考虑一个十六进制数 0725H。 这个数字需要两个字节的内存。 高位字节或最高有效字节为 07,低位字节为 25。
处理器以相反的字节顺序存储数据,即低位字节存储在低内存地址中,高位字节存储在高内存地址中。 因此,如果处理器将值 0725H 从寄存器传输到内存,它将首先将 25 传输到较低的内存地址,然后将 07 传输到下一个内存地址。
x:内存地址
当处理器从内存获取数字数据到寄存器时,它会再次反转字节。 内存地址有两种 −
- 绝对地址 - 具体位置的直接引用。
- 段地址(或偏移量)- 具有偏移值的内存段的起始地址。
汇编程序可以分为三个部分 −
- data 部分,
- bss 部分, and
- text 部分.
data部分
data部分用于声明初始化数据或常量。 该数据在运行时不会改变。 您可以在本节中声明各种常量值、文件名或缓冲区大小等。
声明数据部分的语法是 −
1 | section.data |
bss部分
bss部分用于声明变量。 声明 bss 部分的语法是 −
1 | section.bss |
text部分
text部分用于保存实际代码。 此部分必须以声明 global _start 开头,它告诉内核程序执行的开始位置。
声明文本部分的语法是 −
1 | section.text |
注释
汇编语言注释以分号 (;) 开头。 它可以包含任何可打印字符,包括空白。 它可以单独出现在一行上,例如 −
1 | ; This program displays a message on screen |
或者,与指令在同一行,例如 −
1 | add eax, ebx ; adds ebx to eax |
汇编语言语句
汇编语言程序由三种类型的语句组成 −
- 可执行指令或说明,
- 汇编器指令或伪操作,以及
- 宏。
可执行指令或简单的指令告诉处理器要做什么。 每条指令由一个操作码(opcode)组成。 每条可执行指令生成一条机器语言指令。
汇编程序指令或伪操作告诉汇编程序有关汇编过程的各个方面。 它们是不可执行的,并且不会生成机器语言指令。
宏基本上是一种文本替换机制。
汇编语言语句的语法
汇编语言语句每行输入一个语句。 每个语句都遵循以下格式 −
1 | [label] mnemonic [operands] [;comment] |
方括号中的字段是可选的。 基本指令由两部分组成,第一部分是要执行的指令名称(或助记符),第二部分是命令的操作数或参数。
示例代码:
1 | section .data |
这是一个简单的汇编程序,它将字符串"Hello, world!"打印到标准输出,并退出。
内存段
分段内存模型将系统内存划分为由位于段寄存器中的指针引用的独立段组。 每个段用于包含特定类型的数据。 其中一个段用于包含指令代码,另一段用于存储数据元素,第三个段用于保存程序堆栈。
根据上面的讨论,我们可以将各种内存段指定为 −
-
数据段 − 它由 .data 部分和 .bss 表示。.data 部分用于声明内存区域,其中为程序存储数据元素。 该部分在数据元素声明后无法扩展,并且在整个程序中保持静态。
.bss 部分也是一个静态内存部分,其中包含稍后在程序中声明的数据的缓冲区。 该缓冲存储器被零填充。
-
代码段 − 它由 .text 部分表示。 这定义了内存中存储指令代码的区域。 这也是一个固定区域。
-
堆栈 − 该段包含传递给程序内的函数和过程的数据值。
汇编 - 寄存器
处理器操作主要涉及处理数据。 该数据可以存储在存储器中并从存储器中访问。 然而,从内存中读取数据和将数据存储到内存中会降低处理器的速度,因为它涉及通过控制总线发送数据请求并进入内存存储单元并通过同一通道获取数据的复杂过程。
为了加速处理器操作,处理器包含一些内部存储器存储位置,称为寄存器。
寄存器存储数据元素以进行处理,而无需访问内存。 处理器芯片中内置了有限数量的寄存器。
处理器寄存器
IA-32 架构中有 10 个 32 位和 6 个 16 位处理器寄存器。 寄存器分为三类
- 通用寄存器,
- 控制寄存器,
- 段寄存器。
通用寄存器进一步分为以下几组
- 数据寄存器,
- 指针寄存器,
- 索引寄存器。
数据寄存器
四个 32 位数据寄存器用于算术、逻辑和其他运算。 这些 32 位寄存器可以通过三种方式使用 −
- 作为完整的32位数据寄存器:EAX、EBX、ECX、EDX。
- 32 位寄存器的下半部分可用作四个 16 位数据寄存器:AX、BX、CX 和 DX。
- 上述4个16位寄存器的下半部分和上半部分可以用作8个8位数据寄存器:AH、AL、BH、BL、CH、CL、DH和DL。
其中一些数据寄存器在算术运算中具有特定用途。
AX 为主累加器; 它用于输入/输出和大多数算术指令。 例如,在乘法运算中,根据操作数的大小将一个操作数存储在EAX或AX或AL寄存器中。
BX 被称为基址寄存器,因为它可用于索引寻址。
CX 被称为计数寄存器,与 ECX 一样,CX 寄存器存储迭代操作中的循环计数。
DX被称为数据寄存器。 它还用于输入/输出操作。 它还与 AX 寄存器和 DX 一起使用,用于涉及大值的乘法和除法运算。
指针寄存器
指针寄存器是32位EIP、ESP和EBP寄存器以及相应的16位右部分IP、SP和BP。 指针寄存器分为三类
- 指令指针(IP) − 16位IP寄存器存储下一条要执行的指令的偏移地址。 IP 与 CS 寄存器(如 CS:IP)关联给出了代码段中当前指令的完整地址。
- 堆栈指针(SP) − 16 位 SP 寄存器提供程序堆栈内的偏移值。 SP 与 SS 寄存器 (SS:SP) 相关,指的是程序堆栈中数据或地址的当前位置。
- 基址指针(BP) − 16 位 BP 寄存器主要帮助引用传递给子程序的参数变量。 SS寄存器中的地址与BP中的偏移量相结合,得到参数的位置。 BP还可以与DI、SI组合作为基址寄存器进行特殊寻址。
索引寄存器
32 位索引寄存器、ESI 和 EDI 及其 16 位最右边部分。 SI和DI用于索引寻址,有时用于加法和减法。 有两组索引指针 −
- 来源索引 (SI) +minus; 它用作字符串操作的源索引。
- 目的地索引 (DI) − 它用作字符串操作的目标索引。
控制寄存器
32位指令指针寄存器和32位标志寄存器组合起来被视为控制寄存器。
许多指令涉及比较和数学计算,并更改标志的状态,而其他一些条件指令会测试这些状态标志的值,以将控制流带到其他位置。
常见的标志位是:
- 溢出标志(OF) − 表示有符号算术运算后数据的高位(最左位)溢出。
- 方向标志 (DF) − 它确定移动或比较字符串数据的左或右方向。 当DF值为0时,字符串操作采取从左到右的方向,当该值设置为1时,字符串操作采取从右到左的方向。
- 中断标志(IF) − 它决定是否忽略或处理键盘输入等外部中断。 当该值为 0 时禁用外部中断,当设置为 1 时启用中断。
- 陷阱标志 (TF) − 它允许将处理器的操作设置为单步模式。 我们使用的 DEBUG 程序设置了陷阱标志,因此我们可以一次单步执行一条指令。
- 符号标志(SF) − 它显示算术运算结果的符号。 该标志是根据算术运算之后的数据项的符号来设置的。 符号由最左边位的高位表示。 正结果将 SF 的值清除为 0,负结果将其设置为 1。
- 零标志 (ZF) − 它表示算术或比较运算的结果。 非零结果将零标志清除为 0,零结果将其设置为 1。
- 辅助进位标志 (AF) − 它包含算术运算后从位 3 到位 4 的进位; 用于专门的算术。 当 1 字节算术运算导致从位 3 进位到位 4 时,AF 被置位。
- 奇偶校验标志 (PF) − 表示算术运算结果中1位的总数。 偶数个 1 位将奇偶校验标志清除为 0,奇数个 1 位将奇偶校验标志设置为 1。
- 进位标志(CF) − 它包含算术运算后从高位(最左边)进位 0 或 1。 它还存储shift或rotate操作的最后一位的内容。
下表表示16位Flags寄存器中标志位的位置:
标志位: O D I T S Z A P C Bit no: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 段寄存器
段是程序中定义的用于包含数据、代码和堆栈的特定区域。 主要分为三个部分−
- 代码段 − 它包含所有要执行的指令。 16位代码段寄存器或CS寄存器存储代码段的起始地址。
- 数据段 − 它包含数据、常量和工作区。 16位数据段寄存器或DS寄存器存储数据段的起始地址。
- 堆栈段 − 它包含过程或子例程的数据和返回地址。 它被实现为"堆栈"数据结构。 堆栈段寄存器或SS寄存器存储堆栈的起始地址。
除了 DS、CS 和 SS 寄存器外,还有其他额外段寄存器 - ES(额外段)、FS 和 GS,它们提供额外的段来存储数据。
在汇编编程中,程序需要访问内存位置。 段内的所有内存位置都相对于段的起始地址。 段从可被 16 或十六进制 10 整除的地址开始。因此,所有此类内存地址中最右边的十六进制数字都是 0,通常不存储在段寄存器中。
段寄存器存储段的起始地址。 为了获得段内数据或指令的准确位置,需要偏移值(或位移)。 为了引用段中的任何内存位置,处理器将段寄存器中的段地址与该位置的偏移值组合起来。
汇编 - 寻址模式
大多数汇编语言指令都需要处理操作数。 操作数地址提供了存储要处理的数据的位置。 一些指令不需要操作数,而另一些指令可能需要一个、两个或三个操作数。
当一条指令需要两个操作数时,第一个操作数通常是目标,其中包含寄存器或内存位置中的数据,第二个操作数是源。 源包含要传送的数据(立即寻址)或数据的地址(在寄存器或内存中)。 一般情况下,操作后源数据不会发生变化。
三种基本的寻址模式是 −
- 寄存器寻址
- 立即寻址
- 内存寻址
寄存器寻址
在此寻址模式下,寄存器包含操作数。 根据指令的不同,寄存器可能是第一个操作数、第二个操作数或两者。
例如,
1
2
3
4MOV DX, TAX_RATE ;在第一个操作数中注册
MOV COUNT, CX ;在第二个操作数中注册
MOV EAX, EBX ;两个操作数都在寄存器中
由于在寄存器之间处理数据不涉及内存,因此它提供了最快的数据处理速度。
立即寻址
立即操作数具有常量值或表达式。 当具有两个操作数的指令使用立即寻址时,第一个操作数可以是寄存器或存储器位置,第二操作数是立即常量。 第一个操作数定义数据的长度。
例如,
1
2
3
4
5BYTE_VALUE DB 150 ; A byte value is defined
WORD_VALUE DW 300 ; A word value is defined
ADD BYTE_VALUE, 65 ; An immediate operand 65 is added
MOV AX, 45H ; Immediate constant 45H is transferred to AX
直接内存寻址
当操作数以内存寻址方式指定时,需要直接访问主存,通常是数据段。 这种寻址方式会导致数据处理速度变慢。 为了定位内存中数据的准确位置,我们需要段起始地址(通常在 DS 寄存器中找到)和偏移值。 该偏移值也称为有效地址。
在直接寻址模式下,偏移值直接指定为指令的一部分,通常由变量名指示。 汇编器计算偏移值并维护一个符号表,其中存储了程序中使用的所有变量的偏移值。
在直接内存寻址中,一个操作数引用内存位置,另一个操作数引用寄存器。
例如,
1
2
3ADD BYTE_VALUE, DL ; Adds the register in the memory location
MOV BX, WORD_VALUE ; Operand from the memory is added to register
直接偏移寻址
此寻址模式使用算术运算符来修改地址。 例如,查看以下定义数据表的定义 −
1
2
3BYTE_TABLE DB 14, 15, 22, 45 ; Tables of bytes
WORD_TABLE DW 134, 345, 564, 123 ; Tables of words
以下操作将内存中的表中的数据访问到寄存器中 −
1
2
3
4
5MOV CL, BYTE_TABLE[2] ; Gets the 3rd element of the BYTE_TABLE
MOV CL, BYTE_TABLE + 2 ; Gets the 3rd element of the BYTE_TABLE
MOV CX, WORD_TABLE[3] ; Gets the 4th element of the WORD_TABLE
MOV CX, WORD_TABLE + 3 ; Gets the 4th element of the WORD_TABLE
间接内存寻址
此寻址模式利用计算机的段:偏移寻址能力。 通常,基址寄存器 EBX、EBP(或 BX、BP)和索引寄存器(DI、SI)被编码在方括号内用于存储器引用,用于此目的。
间接寻址通常用于包含多个元素的变量,例如数组。 数组的起始地址存储在 EBX 寄存器中。
以下代码片段显示了如何访问变量的不同元素。
1
2
3
4
5
6MY_TABLE TIMES 10 DW 0 ; Allocates 10 words (2 bytes) each initialized to 0
MOV EBX, [MY_TABLE] ; Effective Address of MY_TABLE in EBX
MOV [EBX], 110 ; MY_TABLE[0] = 110
ADD EBX, 2 ; EBX = EBX +2
MOV [EBX], 123 ; MY_TABLE[1] = 123
MOV 指令
我们已经使用了 MOV 指令,用于将数据从一个存储空间移动到另一个存储空间。 MOV 指令需要两个操作数。
语法
MOV指令的语法为 −
1
MOV destination, source
MOV 指令可能有以下五种形式之一
1
2
3
4
5
6MOV register, register
MOV register, immediate
MOV memory, immediate
MOV register, memory
MOV memory, register
请注意−
- MOV 运算中的两个操作数的大小应相同
- 源操作数的值保持不变
MOV 指令有时会引起歧义。 例如,看一下语句 −
1
2
3MOV EBX, [MY_TABLE] ; Effective Address of MY_TABLE in EBX
MOV [EBX], 110 ; MY_TABLE[0] = 110
不清楚您是否要移动数字 110 的字节等效项或字等效项。在这种情况下,明智的做法是使用类型说明符。
下表显示了一些常见的类型说明符 −
类型说明符 寻址字节数 BYTE 1 WORD 2 DWORD 4 QWORD 8 TBYTE 10 示例
以下程序说明了上面讨论的一些概念。 它将名称"Zara Ali"存储在内存的数据部分中,然后以编程方式将其值更改为另一个名称"Nuha Ali"并显示这两个名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26section .text
global _start ;must be declared for linker (ld)
_start: ;tell linker entry point
;writing the name 'Zara Ali'
mov edx,9 ;message length
mov ecx, name ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov [name], dword 'Nuha' ; Changed the name to Nuha Ali
;writing the name 'Nuha Ali'
mov edx,8 ;message length
mov ecx,name ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
section .data
name db 'Zara Ali '
当上面的代码被编译并执行时,会产生以下结果 −
1
Zara Ali Nuha Ali
汇编 - 变量
NASM 提供了各种define 指令来为变量保留存储空间。 定义汇编指令用于分配存储空间。 它可用于保留和初始化一个或多个字节。
为初始化数据分配存储空间
初始化数据的存储分配语句的语法为 −
1
[variable-name] define-directive initial-value [,initial-value]...
其中,variable-name是每个存储空间的标识符。 汇编器为数据段中定义的每个变量名称关联一个偏移值。
define 指令有五种基本形式 −
指令 用途 存储空间 DB 定义 Byte 分配1个字节 DW 定义 Word 分配2个字节 DD 定义 Doubleword 分配4个字节 DQ 定义 Quadword 分配8个字节 DT 定义十个字节 分配10个字节 以下是使用定义指令的一些示例 −
1
2
3
4
5
6
7choice DB 'y'
number DW 12345
neg_number DW -12345
big_number DQ 123456789
real_number1 DD 1.234
real_number2 DQ 123.456
请注意 −
- 字符的每个字节都以其十六进制的 ASCII 值存储。
- 每个十进制值都会自动转换为其 16 位二进制等效值并存储为十六进制数。
- 处理器使用小尾数字节排序。
- 负数将转换为其 2 的补码表示形式。
- 短浮点数和长浮点数分别使用 32 位或 64 位表示。
下面的程序展示了define指令的使用 −
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16.text
;must be declared for linker (gcc) _start
_start: ;tell linker entry point
mov edx,1 ;message length
mov ecx,choice ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
mov eax,1 ;system call number (sys_exit)
int 0x80 ;call kernel
.data
choice DB 'y'
当上面的代码被编译并执行时,会产生以下结果 −
1
y
为未初始化数据分配存储空间
保留指令用于为未初始化的数据保留空间。 保留指令采用单个操作数来指定要保留的空间单位数。 每个定义指令都有一个相关的保留指令。
保留指令有五种基本形式 −
指令 用途 RESB 保留一个 Byte RESW 保留一个 Word RESD 保留一个 Doubleword RESQ 保留一个 Quadword REST 预留十个字节 多重定义
一个程序中可以有多个数据定义语句。 例如 −
1
2
3
4choice DB 'Y' ;ASCII of y = 79H
number1 DW 12345 ;12345D = 3039H
number2 DD 12345679 ;123456789D = 75BCD15H
汇编器为多个变量定义分配连续的内存。
多次初始化
TIMES 指令允许对同一值进行多次初始化。 例如,可以使用以下语句定义一个名为marks、大小为 9 的数组并将其初始化为零 −
1
marks TIMES 9 DW 0
汇编 - 常量
NASM 提供了几个定义常量的指令。 我们已经在前面的章节中使用过 EQU 指令。 我们将特别讨论三个指令 −
- EQU
- %assign
- %define
EQU 指令
EQU指令用于定义常量。 EQU指令的语法如下 −
1 | CONSTANT_NAME expression |
例如,
1 | TOTAL_STUDENTS equ 50 |
然后您可以在代码中使用这个常量值,例如
1 | mov ecx, TOTAL_STUDENTS |
EQU 语句的操作数可以是表达式
1 | LENGTH equ 20 |
以上代码段将 AREA 定义为 200。
示例
以下示例说明了 EQU 指令的使用 −
1 | SYS_EXIT equ 1 |
当上面的代码被编译并执行时,会产生以下结果 −
1 | Hello, programmers! |
%assign 指令
%assign 指令可用于定义数字常量,如 EQU 指令。 该指令允许重新定义。 例如,您可以将常量 TOTAL 定义为 −
1 | %assign TOTAL 10 |
在代码的后面,您可以将其重新定义为 −
1 | %assign TOTAL 20 |
该指令区分大小写。
%define 指令
%define指令允许定义数字和字符串常量。 该指令类似于 C 中的#define。例如,您可以将常量 PTR 定义为 −
1 | PTR [EBP+4] |
以上代码将PTR替换为[EBP+4]。
该指令还允许重新定义并且区分大小写。