组装 - 快速指南
装配 - 简介
什么是汇编语言?
每台个人计算机都有一个微处理器,用于管理计算机的算术、逻辑和控制活动。
每个处理器系列都有自己的一组指令,用于处理各种操作,例如从键盘获取输入、在屏幕上显示信息以及执行各种其他作业。这些指令集称为“机器语言指令”。
处理器只能理解机器语言指令,即由 1 和 0 组成的字符串。然而,机器语言对于软件开发来说过于晦涩和复杂。因此,低级汇编语言是为特定的处理器系列而设计的,它以符号代码和更易于理解的形式表示各种指令。
汇编语言的优点
了解汇编语言可以让人意识到 -
- 程序如何与操作系统、处理器和 BIOS 连接;
- 数据如何在内存和其他外部设备中表示;
- 处理器如何访问和执行指令;
- 指令如何访问和处理数据;
- 程序如何访问外部设备。
使用汇编语言的其他优点是 -
它需要更少的内存和执行时间;
它以更简单的方式允许特定于硬件的复杂作业;
适合时间要求严格的工作;
它最适合编写中断服务程序和其他内存驻留程序。
PC硬件的基本特征
PC的主要内部硬件由处理器、存储器和寄存器组成。寄存器是保存数据和地址的处理器组件。为了执行程序,系统将其从外部设备复制到内部存储器中。处理器执行程序指令。
计算机存储的基本单位是位;它可以是 ON (1) 或 OFF (0),在大多数现代计算机上,一组 8 个相关位构成一个字节。
因此,奇偶校验位用于使一个字节中的位数为奇数。如果奇偶校验为偶数,则系统假定存在奇偶校验错误(尽管很少见),这可能是由于硬件故障或电气干扰引起的。
处理器支持以下数据大小 -
- Word:2字节数据项
- 双字:4 字节(32 位)数据项
- 四字:8 字节(64 位)数据项
- 段落:16字节(128位)区域
- 千字节:1024 字节
- 兆字节:1,048,576 字节
二进制数系统
每个数字系统都使用位置表示法,即写入数字的每个位置都有不同的位置值。每个位置都是底数的幂,对于二进制数系统来说是2,这些幂从0开始并以1递增。
下表显示了 8 位二进制数的位置值,其中所有位均设置为 ON。
位值 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|---|---|---|
位置值以 2 为底数的幂 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
位数 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
二进制数的值基于 1 位的存在及其位置值。因此,给定二进制数的值为 -
1 + 2 + 4 + 8 +16 + 32 + 64 + 128 = 255
与 2 8 - 1相同。
十六进制数系统
十六进制数字系统使用基数 16。该系统中的数字范围为 0 到 15。按照惯例,字母 A 到 F 用于表示与十进制值 10 到 15 对应的十六进制数字。
计算中的十六进制数用于缩写冗长的二进制表示形式。基本上,十六进制数字系统通过将每个字节分成两半并表达每个半字节的值来表示二进制数据。下表提供了十进制、二进制和十六进制等效值 -
十进制数 | 二进制表示 | 十六进制表示 |
---|---|---|
0 | 0 | 0 |
1 | 1 | 1 |
2 | 10 | 2 |
3 | 11 | 3 |
4 | 100 | 4 |
5 | 101 | 5 |
6 | 110 | 6 |
7 | 111 | 7 |
8 | 1000 | 8 |
9 | 1001 | 9 |
10 | 1010 | A |
11 | 1011 | 乙 |
12 | 1100 | C |
13 | 1101 | D |
14 | 1110 | 乙 |
15 | 1111 | F |
要将二进制数转换为其等效的十六进制数,请将其从右侧开始分成 4 个连续组,并将这些组写在十六进制数的相应数字上。
示例- 二进制数 1000 1100 1101 0001 相当于十六进制 - 8CD1
要将十六进制数转换为二进制数,只需将每个十六进制数字写入其 4 位二进制数即可。
示例- 十六进制数 FAD8 相当于二进制 - 1111 1010 1101 1000
二进制算术
下表说明了二进制加法的四个简单规则 -
(我) | (二) | (三) | (四) |
---|---|---|---|
1 | |||
0 | 1 | 1 | 1 |
+0 | +0 | +1 | +1 |
=0 | =1 | =10 | =11 |
规则 (iii) 和 (iv) 显示将 1 位进位到下一个左侧位置。
例子
十进制 | 二进制 |
---|---|
60 | 00111100 |
+42 | 00101010 |
102 | 01100110 |
负二进制值以二进制补码表示法表示。根据这个规则,将二进制数转换为负值就是将其位值反转并加 1。
例子
53号 | 00110101 |
反转位 | 11001010 |
添加 1 | 0000000 1 |
编号-53 | 11001011 |
要从一个值中减去另一个值,请将要减去的数字转换为二进制补码格式,然后将数字相加。
例子
53 减 42
53号 | 00110101 |
42号 | 00101010 |
反转 42 的位 | 11010101 |
添加 1 | 0000000 1 |
编号-42 | 11010110 |
53 - 42 = 11 | 00001011 |
最后 1 位的溢出丢失。
寻址内存中的数据
处理器控制指令执行的过程称为取指-解码-执行周期或执行周期。它由三个连续的步骤组成 -
- 从内存中取出指令
- 解码或识别指令
- 执行指令
处理器一次可以访问一个或多个字节的存储器。让我们考虑一个十六进制数 0725H。这个数字需要两个字节的内存。高位字节或最高有效字节是 07,低位字节是 25。
处理器以相反的字节顺序存储数据,即低位字节存储在低内存地址中,高位字节存储在高内存地址中。因此,如果处理器将值 0725H 从寄存器传输到内存,它将首先将 25 传输到较低的内存地址,然后将 07 传输到下一个内存地址。
x:内存地址
当处理器从内存获取数字数据进行寄存器时,它会再次反转字节。有两种内存地址 -
绝对地址——具体位置的直接引用。
段地址(或偏移量)- 具有偏移值的内存段的起始地址。
装配 - 环境设置
本地环境设置
汇编语言取决于指令集和处理器的体系结构。在本教程中,我们重点关注 Intel-32 处理器,例如 Pentium。要遵循本教程,您将需要 -
- IBM PC 或任何同等的兼容计算机
- Linux操作系统的副本
- NASM 汇编程序的副本
有许多好的汇编程序,例如 -
- 微软汇编器 (MASM)
- Borland Turbo 汇编器 (TASM)
- GNU 汇编器 (GAS)
我们将使用 NASM 汇编器,因为它是 -
- 自由的。您可以从各种网络资源下载它。
- 有据可查,您将在网上获得大量信息。
- 可以在 Linux 和 Windows 上使用。
安装NASM
如果您在安装Linux时选择“开发工具”,则NASM可能会与Linux操作系统一起安装,无需单独下载和安装。要检查您是否已安装 NASM,请执行以下步骤 -
打开 Linux 终端。
键入whereis nasm并按 ENTER。
如果已安装,则会出现类似nasm: /usr/bin/nasm的行。否则,您将只看到nasm:,那么您需要安装 NASM。
要安装 NASM,请执行以下步骤 -
检查The netwide assembler (NASM)网站以获取最新版本。
下载 Linux 源存档
nasm-X.XX.ta.gz
,其中X.XX
是存档中的 NASM 版本号。将存档解压到一个目录中,该目录会创建一个子目录
nasm-X. XX
。cd
nasm-X.XX
并输入./configure。这个 shell 脚本将找到最好的 C 编译器来使用并相应地设置 Makefiles。键入make以构建 nasm 和 ndisasm 二进制文件。
键入make install以在 /usr/local/bin 中安装 nasm 和 ndisasm 并安装手册页。
这应该在您的系统上安装 NASM。或者,您可以使用 Fedora Linux 的 RPM 发行版。该版本安装比较简单,只需双击RPM文件即可。
汇编 - 基本语法
汇编程序可以分为三个部分 -
数据部分,
bss部分,以及
正文部分。
数据部分_
数据部分用于声明初始化数据或常量。该数据在运行时不会改变。您可以在本节中声明各种常量值、文件名或缓冲区大小等。
声明数据部分的语法是 -
section.data
bss部分_
bss部分用于声明变量。声明 bss 部分的语法是 -
section.bss
正文部分_
文本部分用于保留实际代码。此部分必须以声明global _start开始,它告诉内核程序执行的开始位置。
声明文本部分的语法是 -
section.text global _start _start:
评论
汇编语言注释以分号 (;) 开头。它可以包含任何可打印字符,包括空白。它可以单独出现在一行上,例如 -
; This program displays a message on screen
或者,与指令在同一行,例如 -
add eax, ebx ; adds ebx to eax
汇编语言语句
汇编语言程序由三种类型的语句组成 -
- 可执行指令或说明,
- 汇编指令或伪操作,以及
- 宏。
可执行指令或简单指令告诉处理器要做什么。每条指令由一个操作码(opcode)组成。每条可执行指令生成一条机器语言指令。
汇编程序指令或伪操作告诉汇编程序有关汇编过程的各个方面。它们是不可执行的,并且不会生成机器语言指令。
宏基本上是一种文本替换机制。
汇编语言语句的语法
汇编语言语句每行输入一个语句。每个语句都遵循以下格式 -
[label] mnemonic [operands] [;comment]
方括号中的字段是可选的。基本指令由两部分组成,第一部分是要执行的指令名称(或助记符),第二部分是命令的操作数或参数。
以下是典型汇编语言语句的一些示例 -
INC COUNT ; Increment the memory variable COUNT MOV TOTAL, 48 ; Transfer the value 48 in the ; memory variable TOTAL ADD AH, BH ; Add the content of the ; BH register into the AH register AND MASK1, 128 ; Perform AND operation on the ; variable MASK1 and 128 ADD MARKS, 10 ; Add 10 to the variable MARKS MOV AL, 10 ; Transfer the value 10 to the AL register
汇编中的 Hello World 程序
以下汇编语言代码在屏幕上显示字符串“Hello World” -
section .text global _start ;must be declared for linker (ld) _start: ;tells linker entry point mov edx,len ;message length mov ecx,msg ;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 msg db 'Hello, world!', 0xa ;string to be printed len equ $ - msg ;length of the string
当上面的代码被编译并执行时,它会产生以下结果 -
Hello, world!
在 NASM 中编译和链接汇编程序
确保您已在 PATH 环境变量中设置nasm和ld二进制文件的路径。现在,按照以下步骤编译和链接上述程序 -
使用文本编辑器输入上述代码并将其另存为 hello.asm。
确保您与保存hello.asm 的目录位于同一目录中。
要汇编程序,请输入nasm -f elf hello.asm
如果有任何错误,系统会在此阶段提示您。否则,将创建一个名为hello.o的程序目标文件。
要链接目标文件并创建名为 hello 的可执行文件,请输入ld -m elf_i386 -s -o hello hello.o
输入./hello执行程序
如果您已正确完成所有操作,它将显示“Hello, world!” 屏幕上。
组装 - 内存段
我们已经讨论了装配程序的三个部分。这些部分也代表各种内存段。
有趣的是,如果将section关键字替换为segment,您将得到相同的结果。尝试以下代码 -
segment .text ;code segment global _start ;must be declared for linker _start: ;tell linker entry point mov edx,len ;message length mov ecx,msg ;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 segment .data ;data segment msg db 'Hello, world!',0xa ;our dear string len equ $ - msg ;length of our dear string
当上面的代码被编译并执行时,它会产生以下结果 -
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) - 它用作字符串操作的源索引。
目标索引 (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。它还存储移位或循环操作的最后一位的内容。
下表列出了 16 位标志寄存器中标志位的位置:
旗帜: | 氧 | D | 我 | 时间 | S | Z | A | 磷 | C | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
位号: | 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,通常不存储在段寄存器中。
段寄存器存储段的起始地址。为了获得段内数据或指令的准确位置,需要偏移值(或位移)。为了引用段中的任何内存位置,处理器将段寄存器中的段地址与该位置的偏移值组合起来。
例子
看下面的简单程序来了解寄存器在汇编编程中的使用。该程序在屏幕上显示 9 颗星以及一条简单的消息 -
section .text global _start ;must be declared for linker (gcc) _start: ;tell linker entry point mov edx,len ;message length mov ecx,msg ;message to write mov ebx,1 ;file descriptor (stdout) mov eax,4 ;system call number (sys_write) int 0x80 ;call kernel mov edx,9 ;message length mov ecx,s2 ;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 msg db 'Displaying 9 stars',0xa ;a message len equ $ - msg ;length of message s2 times 9 db '*'
当上面的代码被编译并执行时,它会产生以下结果 -
Displaying 9 stars *********
汇编 - 系统调用
系统调用是用户空间和内核空间之间接口的API。我们已经使用了系统调用。sys_write和sys_exit,分别用于写入屏幕和退出程序。
Linux 系统调用
您可以在汇编程序中使用 Linux 系统调用。要在程序中使用 Linux 系统调用,您需要执行以下步骤 -
- 将系统调用号放入EAX寄存器中。
- 将系统调用的参数存储在寄存器 EBX、ECX 等中。
- 调用相关中断(80h)。
- 结果通常返回到 EAX 寄存器中。
有六个寄存器存储所使用的系统调用的参数。它们是 EBX、ECX、EDX、ESI、EDI 和 EBP。这些寄存器采用连续的参数,从 EBX 寄存器开始。如果有超过六个参数,则第一个参数的内存位置存储在 EBX 寄存器中。
以下代码片段显示了系统调用 sys_exit 的使用 -
mov eax,1 ; system call number (sys_exit) int 0x80 ; call kernel
以下代码片段显示了系统调用 sys_write 的使用 -
mov edx,4 ; message length mov ecx,msg ; message to write mov ebx,1 ; file descriptor (stdout) mov eax,4 ; system call number (sys_write) int 0x80 ; call kernel
所有系统调用及其编号(在调用 int 80h 之前放入 EAX 中的值)都列在/usr/include/asm/unistd.h中。
下表显示了本教程中使用的一些系统调用 -
%eax | 姓名 | %ebx | %ecx | %edx | %esx | %edi |
---|---|---|---|---|---|---|
1 | 系统退出 | 整数 | - | - | - | - |
2 | 系统分叉 | 结构体pt_regs | - | - | - | - |
3 | 系统读取 | 无符号整数 | 字符* | 尺寸_t | - | - |
4 | 系统写入 | 无符号整数 | 常量字符* | 尺寸_t | - | - |
5 | 系统打开 | 常量字符* | 整数 | 整数 | - | - |
6 | 系统关闭 | 无符号整数 | - | - | - | - |
例子
以下示例从键盘读取数字并将其显示在屏幕上 -
section .data ;Data segment userMsg db 'Please enter a number: ' ;Ask the user to enter a number lenUserMsg equ $-userMsg ;The length of the message dispMsg db 'You have entered: ' lenDispMsg equ $-dispMsg section .bss ;Uninitialized data num resb 5 section .text ;Code Segment global _start _start: ;User prompt mov eax, 4 mov ebx, 1 mov ecx, userMsg mov edx, lenUserMsg int 80h ;Read and store the user input mov eax, 3 mov ebx, 2 mov ecx, num mov edx, 5 ;5 bytes (numeric, 1 for sign) of that information int 80h ;Output the message 'The entered number is: ' mov eax, 4 mov ebx, 1 mov ecx, dispMsg mov edx, lenDispMsg int 80h ;Output the number entered mov eax, 4 mov ebx, 1 mov ecx, num mov edx, 5 int 80h ; Exit code mov eax, 1 mov ebx, 0 int 80h
当上面的代码被编译并执行时,它会产生以下结果 -
Please enter a number: 1234 You have entered:1234
汇编 - 寻址模式
大多数汇编语言指令都需要处理操作数。操作数地址提供了存储要处理的数据的位置。一些指令不需要操作数,而另一些指令可能需要一个、两个或三个操作数。
当指令需要两个操作数时,第一个操作数通常是目标,其中包含寄存器或内存位置中的数据,第二个操作数是源。源包含要传送的数据(立即寻址)或数据的地址(在寄存器或内存中)。通常,操作后源数据保持不变。
寻址的三种基本模式是 -
- 寄存器寻址
- 立即寻址
- 内存寻址
寄存器寻址
在这种寻址模式下,寄存器包含操作数。根据指令,寄存器可以是第一操作数、第二操作数或两者。
例如,
MOV DX, TAX_RATE ; Register in first operand MOV COUNT, CX ; Register in second operand MOV EAX, EBX ; Both the operands are in registers
由于在寄存器之间处理数据不涉及内存,因此它提供了最快的数据处理速度。
立即寻址
立即数操作数具有常量值或表达式。当具有两个操作数的指令使用立即寻址时,第一个操作数可以是寄存器或存储器位置,第二操作数是立即常量。第一个操作数定义数据的长度。
例如,
BYTE_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 寄存器中找到)和偏移值。该偏移值也称为有效地址。
在直接寻址模式下,偏移值直接指定为指令的一部分,通常由变量名指示。汇编器计算偏移值并维护一个符号表,其中存储了程序中使用的所有变量的偏移值。
在直接内存寻址中,一个操作数引用内存位置,另一个操作数引用寄存器。
例如,
ADD BYTE_VALUE, DL ; Adds the register in the memory location MOV BX, WORD_VALUE ; Operand from the memory is added to register
直接偏移寻址
这种寻址模式使用算术运算符来修改地址。例如,查看以下定义数据表的定义 -
BYTE_TABLE DB 14, 15, 22, 45 ; Tables of bytes WORD_TABLE DW 134, 345, 564, 123 ; Tables of words
以下操作将内存中的表中的数据访问到寄存器中 -
MOV 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 寄存器中。
以下代码片段显示了如何访问变量的不同元素。
MY_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 指令的语法是 -
MOV destination, source
MOV 指令可能有以下五种形式之一 -
MOV register, register MOV register, immediate MOV memory, immediate MOV register, memory MOV memory, register
请注意 -
- MOV 运算中的两个操作数应该具有相同的大小
- 源操作数的值保持不变
MOV 指令有时会引起歧义。例如,看一下语句 -
MOV EBX, [MY_TABLE] ; Effective Address of MY_TABLE in EBX MOV [EBX], 110 ; MY_TABLE[0] = 110
目前尚不清楚您是否要移动数字 110 的字节等效项或字等效项。在这种情况下,明智的做法是使用类型说明符。
下表显示了一些常见的类型说明符 -
类型说明符 | 寻址字节数 |
---|---|
字节 | 1 |
单词 | 2 |
双字 | 4 |
Q字 | 8 |
字节 | 10 |
例子
以下程序说明了上面讨论的一些概念。它将名称“Zara Ali”存储在内存的数据部分中,然后以编程方式将其值更改为另一个名称“Nuha Ali”并显示这两个名称。
section .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 '
当上面的代码被编译并执行时,它会产生以下结果 -
Zara Ali Nuha Ali
汇编 - 变量
NASM 提供了各种定义指令来为变量保留存储空间。定义汇编指令用于分配存储空间。它可用于保留和初始化一个或多个字节。
为初始化数据分配存储空间
初始化数据的存储分配语句的语法是 -
[variable-name] define-directive initial-value [,initial-value]...
其中,variable-name是每个存储空间的标识符。汇编器为数据段中定义的每个变量名关联一个偏移值。
Define 指令有五种基本形式 -
指示 | 目的 | 储存空间 |
---|---|---|
D B | 定义字节 | 分配1个字节 |
数据仓库 | 定义词 | 分配2个字节 |
DD | 定义双字 | 分配4个字节 |
数据Q | 定义四字 | 分配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
请注意 -
字符的每个字节都以其十六进制的 ASCII 值存储。
每个十进制值都会自动转换为其 16 位二进制等效值并存储为十六进制数。
处理器使用小端字节顺序。
负数将转换为其 2 的补码表示形式。
短浮点数和长浮点数分别使用 32 位或 64 位表示。
以下程序显示了定义指令的使用 -
section .text global _start ;must be declared for linker (gcc) _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 section .data choice DB 'y'
当上面的代码被编译并执行时,它会产生以下结果 -
y
为未初始化数据分配存储空间
保留指令用于为未初始化的数据保留空间。保留指令采用单个操作数来指定要保留的空间单位数。每个定义指令都有一个相关的保留指令。
储备指令有五种基本形式 -
指示 | 目的 |
---|---|
RESB | 保留一个字节 |
RESW | 保留一个字 |
RESD | 保留双字 |
RESQ | 保留四字 |
休息 | 预留十个字节 |
多重定义
一个程序中可以有多个数据定义语句。例如 -
choice DB 'Y' ;ASCII of y = 79H number1 DW 12345 ;12345D = 3039H number2 DD 12345679 ;123456789D = 75BCD15H
汇编器为多个变量定义分配连续的内存。
多重初始化
TIMES 指令允许对同一值进行多次初始化。例如,可以使用以下语句定义一个名为marks、大小为 9 的数组并将其初始化为零 -
marks TIMES 9 DW 0
TIMES 指令在定义数组和表时很有用。以下程序在屏幕上显示 9 个星号 -
section .text global _start ;must be declared for linker (ld) _start: ;tell linker entry point mov edx,9 ;message length mov ecx, stars ;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 stars times 9 db '*'
当上面的代码被编译并执行时,它会产生以下结果 -
*********
汇编 - 常量
NASM 提供了几个定义常量的指令。我们已经在前面的章节中使用过 EQU 指令。我们将特别讨论三个指令 -
- 均衡器
- %分配
- %定义
EQU 指令
EQU 伪指令用于定义常量。EQU 指令的语法如下 -
CONSTANT_NAME EQU expression
例如,
TOTAL_STUDENTS equ 50
然后您可以在代码中使用这个常量值,例如 -
mov ecx, TOTAL_STUDENTS cmp eax, TOTAL_STUDENTS
EQU 语句的操作数可以是表达式 -
LENGTH equ 20 WIDTH equ 10 AREA equ length * width
上面的代码段将 AREA 定义为 200。
例子
以下示例说明了 EQU 指令的使用 -
SYS_EXIT equ 1 SYS_WRITE equ 4 STDIN equ 0 STDOUT equ 1 section .text global _start ;must be declared for using gcc _start: ;tell linker entry point mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg1 mov edx, len1 int 0x80 mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg2 mov edx, len2 int 0x80 mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg3 mov edx, len3 int 0x80 mov eax,SYS_EXIT ;system call number (sys_exit) int 0x80 ;call kernel section .data msg1 db 'Hello, programmers!',0xA,0xD len1 equ $ - msg1 msg2 db 'Welcome to the world of,', 0xA,0xD len2 equ $ - msg2 msg3 db 'Linux assembly programming! ' len3 equ $- msg3
当上面的代码被编译并执行时,它会产生以下结果 -
Hello, programmers! Welcome to the world of, Linux assembly programming!
%分配指令
% assign指令可用于定义数字常量,如 EQU 指令。该指令允许重新定义。例如,您可以将常量 TOTAL 定义为 -
%assign TOTAL 10
稍后在代码中,您可以将其重新定义为 -
%assign TOTAL 20
该指令区分大小写。
%define 指令
% define指令允许定义数字和字符串常量。该指令类似于 C 中的#define。例如,您可以将常量 PTR 定义为 -
%define PTR [EBP+4]
上述代码将PTR替换为[EBP+4]。
该指令还允许重新定义并且区分大小写。
汇编 - 算术指令
INC 指令
INC 指令用于将操作数加一。它适用于可以位于寄存器或内存中的单个操作数。
句法
INC 指令具有以下语法 -
INC destination
操作数目标可以是 8 位、16 位或 32 位操作数。
例子
INC EBX ; Increments 32-bit register INC DL ; Increments 8-bit register INC [count] ; Increments the count variable
DEC 指令
DEC指令用于将操作数减1。它适用于可以位于寄存器或内存中的单个操作数。
句法
DEC 指令具有以下语法 -
DEC destination
操作数目标可以是 8 位、16 位或 32 位操作数。
例子
segment .data count dw 0 value db 15 segment .text inc [count] dec [value] mov ebx, count inc word [ebx] mov esi, value dec byte [esi]
ADD 和 SUB 指令
ADD和SUB指令用于执行字节、字和双字大小的二进制数据的简单加法/减法,即分别用于8位、16位或32位操作数的加或减。
句法
ADD 和 SUB 指令具有以下语法 -
ADD/SUB destination, source
ADD/SUB 指令可以发生在 -
- 注册即可注册
- 注册内存
- 注册到内存
- 寄存器到常量数据
- 内存到常量数据
然而,与其他指令一样,无法使用 ADD/SUB 指令进行内存到内存操作。ADD 或 SUB 操作设置或清除溢出和进位标志。
例子
下面的示例将询问用户两个数字,分别将数字存储在 EAX 和 EBX 寄存器中,将值相加,将结果存储在内存位置“ res ”中,最后显示结果。
SYS_EXIT equ 1 SYS_READ equ 3 SYS_WRITE equ 4 STDIN equ 0 STDOUT equ 1 segment .data msg1 db "Enter a digit ", 0xA,0xD len1 equ $- msg1 msg2 db "Please enter a second digit", 0xA,0xD len2 equ $- msg2 msg3 db "The sum is: " len3 equ $- msg3 segment .bss num1 resb 2 num2 resb 2 res resb 1 section .text global _start ;must be declared for using gcc _start: ;tell linker entry point mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg1 mov edx, len1 int 0x80 mov eax, SYS_READ mov ebx, STDIN mov ecx, num1 mov edx, 2 int 0x80 mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg2 mov edx, len2 int 0x80 mov eax, SYS_READ mov ebx, STDIN mov ecx, num2 mov edx, 2 int 0x80 mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg3 mov edx, len3 int 0x80 ; moving the first number to eax register and second number to ebx ; and subtracting ascii '0' to convert it into a decimal number mov eax, [num1] sub eax, '0' mov ebx, [num2] sub ebx, '0' ; add eax and ebx add eax, ebx ; add '0' to to convert the sum from decimal to ASCII add eax, '0' ; storing the sum in memory location res mov [res], eax ; print the sum mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, res mov edx, 1 int 0x80 exit: mov eax, SYS_EXIT xor ebx, ebx int 0x80
当上面的代码被编译并执行时,它会产生以下结果 -
Enter a digit: 3 Please enter a second digit: 4 The sum is: 7
带有硬编码变量的程序 -
section .text global _start ;must be declared for using gcc _start: ;tell linker entry point mov eax,'3' sub eax, '0' mov ebx, '4' sub ebx, '0' add eax, ebx add eax, '0' mov [sum], eax mov ecx,msg mov edx, len mov ebx,1 ;file descriptor (stdout) mov eax,4 ;system call number (sys_write) int 0x80 ;call kernel mov ecx,sum mov edx, 1 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 msg db "The sum is:", 0xA,0xD len equ $ - msg segment .bss sum resb 1
当上面的代码被编译并执行时,它会产生以下结果 -
The sum is: 7
MUL/IMUL 指令
有两条指令用于将二进制数据相乘。MUL(乘法)指令处理无符号数据,IMUL(整数乘法)指令处理有符号数据。两条指令都会影响进位和溢出标志。
句法
MUL/IMUL 指令的语法如下 -
MUL/IMUL multiplier
在这两种情况下,被乘数都将位于累加器中,具体取决于被乘数和乘数的大小,并且生成的乘积也取决于操作数的大小存储在两个寄存器中。以下部分解释了三种不SymPy况下的 MUL 指令 -
先生。 | 应用场景 |
---|---|
1 | 当两个字节相乘时 - 被乘数在AL寄存器中,乘数是内存中或另一个寄存器中的一个字节。该产品在 AX 中。乘积的高 8 位存储在 AH 中,低 8 位存储在 AL 中。 |
2 | 当两个单字值相乘时 - 被乘数应该在AX寄存器中,乘数是内存或另一个寄存器中的一个字。例如,对于像 MUL DX 这样的指令,必须将乘数存储在 DX 中,将被乘数存储在 AX 中。 生成的结果是一个双字,需要两个寄存器。高位(最左边)部分存储在 DX 中,低位(最右边)部分存储在 AX 中。 |
3 | 当两个双字值相乘时 - 当两个双字值相乘时,被乘数应位于 EAX 中,乘数是存储在内存或另一个寄存器中的双字值。生成的乘积存储在 EDX:EAX 寄存器中,即高位 32 位存储在 EDX 寄存器中,低位 32 位存储在 EAX 寄存器中。 |
例子
MOV AL, 10 MOV DL, 25 MUL DL ... MOV DL, 0FFH ; DL= -1 MOV AL, 0BEH ; AL = -66 IMUL DL
例子
以下示例将 3 乘以 2,并显示结果 -
section .text global _start ;must be declared for using gcc _start: ;tell linker entry point mov al,'3' sub al, '0' mov bl, '2' sub bl, '0' mul bl add al, '0' mov [res], al mov ecx,msg mov edx, len mov ebx,1 ;file descriptor (stdout) mov eax,4 ;system call number (sys_write) int 0x80 ;call kernel mov ecx,res mov edx, 1 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 msg db "The result is:", 0xA,0xD len equ $- msg segment .bss res resb 1
当上面的代码被编译并执行时,它会产生以下结果 -
The result is: 6
DIV/IDIV 指令
除法运算生成两个元素 -商和余数。在乘法的情况下,不会发生溢出,因为使用双倍长度寄存器来保存乘积。然而,在除法的情况下,可能会发生溢出。如果发生溢出,处理器会生成中断。
DIV(除法)指令用于无符号数据,IDIV(整数除法)指令用于有符号数据。
句法
DIV/IDIV 指令的格式 -
DIV/IDIV divisor
股息位于累加器中。这两个指令都可以使用 8 位、16 位或 32 位操作数。该操作影响所有六个状态标志。以下部分解释了具有不同操作数大小的除法的三种情况 -
先生。 | 应用场景 |
---|---|
1 | 当除数为 1 字节时 - 假定被除数位于 AX 寄存器(16 位)中。除法后,商存入 AL 寄存器,余数存入 AH 寄存器。 |
2 | 当除数为 1 个字时 - 假设被除数为 32 位长,位于 DX:AX 寄存器中。高16位在DX中,低16位在AX中。除法后,16 位商进入 AX 寄存器,16 位余数进入 DX 寄存器。 |
3 | 当除数为双字时 - 假设被除数为 64 位长并位于 EDX:EAX 寄存器中。高阶 32 位在 EDX 中,低阶 32 位在 EAX 中。除法后,32 位商进入 EAX 寄存器,32 位余数进入 EDX 寄存器。 |
例子
以下示例将 8 除以 2。被除数 8存储在16 位 AX 寄存器中,除数 2存储在8 位 BL 寄存器中。
section .text global _start ;must be declared for using gcc _start: ;tell linker entry point mov ax,'8' sub ax, '0' mov bl, '2' sub bl, '0' div bl add ax, '0' mov [res], ax mov ecx,msg mov edx, len mov ebx,1 ;file descriptor (stdout) mov eax,4 ;system call number (sys_write) int 0x80 ;call kernel mov ecx,res mov edx, 1 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 msg db "The result is:", 0xA,0xD len equ $- msg segment .bss res resb 1
当上面的代码被编译并执行时,它会产生以下结果 -
The result is: 4
汇编 - 逻辑指令
处理器指令集提供AND、OR、XOR、TEST和NOT布尔逻辑指令,根据程序的需要测试、设置和清除位。
这些说明的格式 -
先生。 | 操作说明 | 格式 |
---|---|---|
1 | 和 | AND 操作数 1, 操作数 2 |
2 | 或者 | OR 操作数 1, 操作数 2 |
3 | 异或 | 异或操作数 1、操作数 2 |
4 | 测试 | TEST 操作数 1、操作数 2 |
5 | 不是 |