汇编 - 快速指南

汇编 - 简介

什么是汇编语言?

每台个人计算机都有一个微处理器,用于管理计算机的算术、逻辑和控制活动。

每个处理器系列都有自己的一组指令,用于处理各种操作,例如从键盘获取输入、在屏幕上显示信息以及执行各种其他工作。这些指令集称为"机器语言指令"。

处理器只能理解机器语言指令,即 1 和 0 的字符串。但是,机器语言对于软件开发来说太过晦涩和复杂。因此,低级汇编语言是为特定系列的处理器设计的,它以符号代码和更易于理解的形式表示各种指令。

汇编语言的优点

了解汇编语言可以让人意识到−

  • 程序如何与操作系统、处理器和 BIOS 交互;
  • 数据如何在内存和其他外部设备中表示;
  • 处理器如何访问和执行指令;
  • 指令如何访问和处理数据;
  • 程序如何访问外部设备。

使用汇编语言的其他优点是−

  • 它需要更少的内存和执行时间;

  • 它以更简单的方式允许特定于硬件的复杂作业;

  • 它适用于时间关键型作业;

  • 它最适合编写中断服务例程和其他内存驻留程序。

PC 硬件的基本特性

PC 的主要内部硬件由处理器、内存和寄存器组成。寄存器是保存数据和地址的处理器组件。要执行程序,系统会将其从外部设备复制到内部内存中。处理器执行程序指令。

计算机存储的基本单位是位;它可以是 ON (1) 或 OFF (0),在大多数现代计算机上,一组 8 个相关位组成一个字节。

因此,奇偶校验位用于使字节中的位数为奇数。如果奇偶校验为偶数,则系统假定发生了奇偶校验错误(尽管很少见),这可能是由于硬件故障或电气干扰引起的。

处理器支持以下数据大小 −

  • 字:2 字节数据项
  • 双字:4 字节(32 位)数据项
  • 四字:8 字节(64 位)数据项
  • 段:16 字节(128 位)区域
  • 千字节:1024 字节
  • 兆字节:1,048,576 字节

二进制数字系统

每个数字系统都使用位置表示法,即,每个数字所在的位置都有不同的位置值。每个位置都是基数的幂,二进制为 2,这些幂从 0 开始,以 1 为增量。

下表显示了 8 位二进制数的位置值,其中所有位都设置为 ON。

位值 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

与 28 - 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 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

要将二进制数转换为其十六进制等价数,请将其分成 4 个连续组,从右侧开始,并将这些组写在十六进制数的相应数字上。

示例 − 二进制数 1000 1100 1101 0001 相当于十六进制数 - 8CD1

要将十六进制数转换为二进制数,只需将每个十六进制数字写入其 4 位二进制等价数即可。

示例 − 十六进制数 FAD8 相当于二进制数 - 1111 1010 1101 1000

二进制算术

下表说明了二进制加法的四个简单规则 −

(i) (ii) (iii) (iv)
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 00000001
数字-53 11001011

要从另一个值中减去一个值,将要减去的数字转换为二进制补码格式,然后将数字相加

示例

从 53 中减去 42

数字 53 00110101
数字 42 00101010
反转42 11010101
加 1 00000001
数字 -42 11010110
53 - 42 = 11 00001011

最后 1 位溢出丢失。

寻址内存中的数据

处理器控制指令执行的过程称为获取-解码-执行周期执行周期。它由三个连续的步骤组成 −

  • 从内存中获取指令
  • 解码或识别指令
  • 执行指令

处理器一次可以访问一个或多个字节的内存。让我们考虑一个十六进制数 0725H。这个数字将需要两个字节的内存。高位字节或最高有效字节是 07,低位字节是 25。

处理器以反向字节顺序存储数据,即低位字节存储在低内存地址中,高位字节存储在高内存地址中。因此,如果处理器将值 0725H 从寄存器传送到内存,它将首先将 25 传输到较低的内存地址,并将 07 传输到下一个内存地址。

Introduction

x:内存地址

当处理器将数字数据从内存传送到寄存器时,它会再次反转字节。内存地址有两种 −

  • 绝对地址 - 特定位置的直接引用。

  • 段地址(或偏移量) - 具有偏移量的内存段的起始地址。

汇编 - 环境设置

本地环境设置

汇编语言依赖于处理器的指令集和架构。在本教程中,我们重点介绍 Intel-32 处理器,如奔腾。要遵循本教程,您需要 −

  • IBM PC 或任何同等兼容计算机
  • Linux 操作系统的副本
  • NASM 汇编程序的副本

有许多优秀的汇编程序,例如 −

  • Microsoft Assembler (MASM)
  • Borland Turbo Assembler (TASM)
  • GNU 汇编程序 (GAS)

我们将使用 NASM 汇编程序,因为它是 −

  • 免费的。您可以从各种网络资源下载它。
  • 文档齐全,您将在网上获得大量信息。
  • 可以在 Linux 和 Windows 上使用。

安装 NASM

如果在安装 Linux 时选择"开发工具",您可能会与 Linux 操作系统一起安装 NASM,而无需单独下载和安装。要检查您是否已安装 NASM,请执行以下步骤 −

  • 打开 Linux 终端。

  • 输入 whereis nasm 并按 ENTER。

  • 如果已安装,则会出现类似 nasm: /usr/bin/nasm 的行。否则,您只会看到 nasm:,然后您需要安装 NASM。

要安装 NASM,请执行以下步骤 −

  • 检查 网络汇编程序 (NASM) 网站以获取最新版本。

  • 下载 Linux 源代码档案 nasm-X.XX.ta.gz,其中 X.XX 是档案中的 NASM 版本号。

  • 将档案解压到一​​个目录中,该目录将创建子目录 nasm-X。 XX

  • cd 到 nasm-X.XX 并输入 ./configure。此 shell 脚本将找到最佳的 C 编译器并相应地设置 Makefile。

  • 输入 make 以构建 nasm 和 ndisasm 二进制文件。

  • 输入 make install 以在 /usr/local/bin 中安装 nasm 和 ndisasm 并安装手册页。

这应该会在您的系统上安装 NASM。或者,您可以使用 Fedora Linux 的 RPM 发行版。此版本安装更简单,只需双击 RPM 文件即可。

汇编 - 基本语法

汇编程序可分为三个部分 −

  • data 部分,

  • bss 部分,和

  • text 部分。

data 部分

data 部分用于声明初始化数据或常量。此数据在运行时不会更改。您可以在此部分中声明各种常量值、文件名或缓冲区大小等。

声明数据部分的语法是 −

section.data

bss 部分

bss 部分用于声明变量。声明 bss 部分的语法是 −

section.bss

text 部分

text 部分用于保存实际代码。此部分必须以声明 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 环境变量中设置了 nasmld 二进制文件的路径。现在,按照以下步骤编译和链接上述程序 −

  • 使用文本编辑器输入上述代码并将其保存为 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 关键字替换为 section,您将获得相同的结果。尝试以下代码 −

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。

  • 上述四个 16 位寄存器的低半部分和高半部分可用作八个 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 位标志寄存器中标志位的位置:

标志: 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,这通常不存储在段寄存器中。

段寄存器存储段的起始地址。要获取段内数据或指令的确切位置,需要偏移值(或位移)。要引用段中的任何内存位置,处理器会将段寄存器中的段地址与该位置的偏移值相结合。

示例

查看以下简单程序,了解寄存器在汇编编程中的使用。此程序在屏幕上显示 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

所有系统调用均列在 /usr/include/asm/unistd.h 中,并附有其编号(在调用 int 80h 之前放入 EAX 中的值)。

下表显示了本教程中使用的部分系统调用 −

%eax Name %ebx %ecx %edx %esx %edi
1 sys_exit int - - - -
2 sys_fork struct pt_regs - - - -
3 sys_read unsigned int char * size_t - -
4 sys_write unsigned int const char * size_t - -
5 sys_open const char * int int - -
6 sys_close unsigned int - - - -

示例

以下示例从键盘读取一个数字并将其显示在屏幕上 −

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 的字节等价物或字等价物。在这种情况下,最好使用类型说明符

下表显示了一些常见的类型说明符 −

类型说明符 字节寻址
BYTE 1
WORD 2
DWORD 4
QWORD 8
TBYTE 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 提供各种define 指令,用于为变量保留存储空间。define 汇编指令用于分配存储空间。它可用于保留以及初始化一个或多个字节。

为初始化数据分配存储空间

初始化数据的存储分配语句的语法为 −

[variable-name]    define-directive    initial-value   [,initial-value]...

其中,variable-name是每个存储空间的标识符。汇编器为数据段中定义的每个变量名关联一个偏移值。

define 指令 − 有五种基本形式

指令 用途 存储空间
DB 定义字节 分配 1 个字节
DW 定义字 分配 2字节
DD 定义双字 分配 4 个字节
DQ 定义四字 分配 8 个字节
DT 定义十个字节 分配 10 个字节

以下是使用 define 指令的一些示例 −

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 位表示。

以下程序显示了 define 指令的使用 −

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

为未初始化数据分配存储空间

reserve 指令用于为未初始化数据保留空间。reserve 指令采用单个操作数,指定要保留的空间单位数。每个 define 指令都有一个相关的 reserve 指令。

reserve 指令有五种基本形式 −

指令 目的
RESB 保留一个字节
RESW 保留一个字
RESD 保留一个双字
RESQ 保留一个四字
REST 保留十个字节

多个定义

程序中可以有多个数据定义语句。例如 −

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
  • %assign
  • %define

EQU 指令

EQU 指令用于定义常量。EQU 指令的语法如下 −

CONSTANT_NAME EQU 表达式

例如,

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 指令

%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 指令用于将操作数减一。它适用于单个操作数,该操作数可以位于寄存器中或内存中。

语法

DEC 指令具有以下语法 −

DEC destination

操作数目标可以是 8 位、16 位或 32 位操作数。

Example

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

在两种情况下,被乘数都将位于累加器中,具体取决于被乘数和乘数的大小,并且生成的乘积也存储在两个寄存器中,具体取决于操作数的大小。以下部分解释了三种不同情况的 MUL 指令 −

Sr.No. 场景
1

当两个字节相乘时 −

被乘数在 AL 寄存器中,乘数是内存或其他寄存器中的一个字节。乘积在 AX 中。乘积的高 8 位存储在 AH 中,低 8 位存储在 AL 中。

Arithmetic1

2

当两个单字值相乘时 −

被乘数应位于 AX 寄存器中,而乘数是内存或另一个寄存器中的一个字。例如,对于像 MUL DX 这样的指令,您必须将乘数存储在 DX 中,将被乘数存储在 AX 中。

结果乘积是一个双字,需要两个寄存器。高阶(最左边)部分存储在 DX 中,低阶(最右边)部分存储在 AX 中。

Arithmetic2

3

当两个双字值相乘时 −

当两个双字值相乘时,被乘数应在 EAX 中,而乘数是存储在内存或其他寄存器中的双字值。生成的乘积存储在 EDX:EAX 寄存器中,即高 32 位存储在 EDX 寄存器中,低 32 位存储在 EAX 寄存器中。

Arithmetic3

示例

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 位操作数。该操作会影响所有六个状态标志。以下部分解释了三种具有不同操作数大小的除法的情况 −

Sr.No. 场景
1

当除数为 1 字节时 −

假设被除数在 AX 寄存器(16 位)中。除法后,商进入 AL 寄存器,余数进入 AH 寄存器。

Arithmetic4

2 2

当除数为 1 个字时 −

被除数被假定为 32 位长,位于 DX:AX 寄存器中。高 16 位在 DX 中,低 16 位在 AX 中。除法后,16 位商进入 AX 寄存器,16 位余数进入 DX 寄存器。

Arithmetic5

3

当除数为双字 −

被除数被假定为 64 位长,位于 EDX:EAX 寄存器中。高 32 位在 EDX 中,低 32 位在 EAX 中。除法后,32 位商进入 EAX 寄存器,32 位余数进入 EDX 寄存器。

Arithmetic6

示例

以下示例将 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 布尔逻辑,可根据程序的需要测试、设置和清除位。

这些指令的格式 −

Sr.No. 指令 格式
1 AND AND 操作数1,操作数2
2 OR OR 操作数1,操作数2
3 XOR XOR 操作数1,操作数2
4 TEST TEST 操作数1,操作数2
5 NOT NOT 操作数1

所有情况下的第一个操作数都可以在寄存器中,也可以在内存中。第二个操作数可以是寄存器/内存中,也可以是立即数(常量)。但是,内存到内存的操作是不可能的。这些指令比较或匹配操作数的位并设置 CF、OF、PF、SF 和 ZF 标志。

AND 指令

AND 指令用于通过执行按位 AND 运算来支持逻辑表达式。如果两个操作数的匹配位都是 1,则按位 AND 运算返回 1,否则返回 0。例如 −

             Operand1: 	0101
             Operand2: 	0011
----------------------------
After AND -> Operand1:	0001

AND 运算可用于清除一个或多个位。例如,假设 BL 寄存器包含 0011 1010。如果您需要将高位清除为零,请将其与 0FH 进行 AND 运算。

AND	BL,   0FH   ; This sets BL to 0000 1010

我们再举一个例子。如果你想检查一个给定的数字是奇数还是偶数,一个简单的测试就是检查数字的最低有效位。如果这是 1,则数字为奇数,否则数字为偶数。

假设数字在 AL 寄存器中,我们可以写 −

AND	AL, 01H     ; ANDing with 0000 0001
JZ    EVEN_NUMBER

以下程序说明了这一点 −

示例

section .text
   global _start            ;must be declared for using gcc
	
_start:                     ;tell linker entry point
   mov   ax,   8h           ;getting 8 in the ax 
   and   ax, 1              ;and ax with 1
   jz    evnn
   mov   eax, 4             ;system call number (sys_write)
   mov   ebx, 1             ;file descriptor (stdout)
   mov   ecx, odd_msg       ;message to write
   mov   edx, len2          ;length of message
   int   0x80               ;call kernel
   jmp   outprog

evnn:   
  
   mov   ah,  09h
   mov   eax, 4             ;system call number (sys_write)
   mov   ebx, 1             ;file descriptor (stdout)
   mov   ecx, even_msg      ;message to write
   mov   edx, len1          ;length of message
   int   0x80               ;call kernel

outprog:

   mov   eax,1              ;system call number (sys_exit)
   int   0x80               ;call kernel

section   .data
even_msg  db  'Even Number!' ;message showing even number
len1  equ  $ - even_msg 
   
odd_msg db  'Odd Number!'    ;message showing odd number
len2  equ  $ - odd_msg

当上述代码被编译并执行时,它会产生以下结果 −

Even Number!

用奇数更改 ax 寄存器中的值,例如 −

mov  ax, 9h                  ; getting 9 in the ax

程序将显示:

Odd Number!

类似地,要清除整个寄存器,您可以将其与 00H 进行 AND 运算。

OR 指令

OR 指令用于通过执行按位 OR 运算来支持逻辑表达式。如果任一或两个操作数的匹配位为 1,则按位 OR 运算符返回 1。如果两位均为 0,则返回 0。

例如,

             Operand1:     0101
             Operand2:     0011
----------------------------
After OR -> Operand1:    0111

或运算可用于设置一位或多位。例如,假设 AL 寄存器包含 0011 1010,您需要设置四个低位,您可以将其与值 0000 1111(即 FH)进行或运算。

OR BL, 0FH                   ; This sets BL to  0011 1111

示例

以下示例演示了 OR 指令。让我们分别将值 5 和 3 存储在 AL 和 BL 寄存器中,然后执行指令

OR AL, BL

应该将 7 存储在 AL 寄存器中 −

section .text
   global _start            ;must be declared for using gcc
	
_start:                     ;tell linker entry point
   mov    al, 5             ;getting 5 in the al
   mov    bl, 3             ;getting 3 in the bl
   or     al, bl            ;or al and bl registers, result should be 7
   add    al, byte '0'      ;converting decimal to ascii
	
   mov    [result],  al
   mov    eax, 4
   mov    ebx, 1
   mov    ecx, result
   mov    edx, 1 
   int    0x80
    
outprog:
   mov    eax,1             ;system call number (sys_exit)
   int    0x80              ;call kernel
	
section    .bss
result resb 1

当上述代码被编译并执行时,它会产生以下结果 −

7

XOR 指令

XOR 指令实现按位 XOR 运算。当且仅当操作数的位不同时,XOR 运算才会将结果位设置为 1。如果操作数的位相同(均为 0 或均为 1),则结果位将清除为 0。

例如,

             Operand1:     0101
             Operand2:     0011
----------------------------
After XOR -> Operand1:    0110

对操作数进行异或会将操作数更改为0。这用于清除寄存器。

XOR     EAX, EAX

TEST 指令

TEST 指令的工作原理与 AND 运算相同,但与 AND 指令不同的是,它不会改变第一个操作数。因此,如果我们需要检查寄存器中的数字是偶数还是奇数,我们也可以使用 TEST 指令来执行此操作,而无需更改原始数字。

TEST    AL, 01H
JZ      EVEN_NUMBER

NOT 指令

NOT 指令实现按位 NOT 运算。NOT 运算会反转操作数中的位。操作数可以位于寄存器中,也可以位于内存中。

例如,

             Operand1:    0101 0011
After NOT -> Operand1:    1010 1100

汇编 - 条件

汇编语言中的条件执行由多个循环和分支指令完成。这些指令可以改变程序中的控制流。在两种情况下可以观察到条件执行 −

Sr.No. 条件指令
1

无条件跳转

这是由 JMP 指令执行的。条件执行通常涉及将控制转移到不跟在当前执行指令之后的指令的地址。控制转移可以是向前的,以执行一组新的指令,也可以是向后的,以重新执行相同的步骤。

2

条件跳转

这由一组跳转指令 j<condition> 根据条件执行。条件指令通过中断顺序流来转移控制,它们通过更改 IP 中的偏移值来实现。

在讨论条件指令之前,让我们先讨论一下 CMP 指令。

CMP 指令

CMP 指令比较两个操作数。它通常用于条件执行。该指令基本上从另一个操作数中减去一个操作数,以比较操作数是否相等。它不会干扰目标或源操作数。它与条件跳转指令一起使用以进行决策。

语法

CMP destination, source

CMP 比较两个数字数据字段。目标操作数可以在寄存器中,也可以在内存中。源操作数可以是常量(立即数)数据、寄存器或内存。

示例

CMP DX,	00  ; Compare the DX value with zero
JE  L7      ; If yes, then jump to label L7
.
.
L7: ...  

CMP 通常用于比较计数器值是否已达到循环需要运行的次数。考虑以下典型条件 −

INC	EDX
CMP	EDX, 10	; Compares whether the counter has reached 10
JLE	LP1     ; If it is less than or equal to 10, then jump to LP1

无条件跳转

如前所述,这是由 JMP 指令执行的。条件执行通常涉及将控制转移到不跟在当前执行指令后面的指令地址。控制转移可能是向前的,以执行一组新的指令,也可能是向后的,以重新执行相同的步骤。

语法

JMP 指令提供了一个标签名,控制流会立即转移到该标签名。JMP 指令的语法是 −

JMP	label

示例

以下代码片段说明了 JMP 指令 −

MOV  AX, 00    ; Initializing AX to 0
MOV  BX, 00    ; Initializing BX to 0
MOV  CX, 01    ; Initializing CX to 1
L20:
ADD  AX, 01    ; Increment AX
ADD  BX, AX    ; Add AX to BX
SHL  CX, 1     ; shift left CX, this in turn doubles the CX value
JMP  L20       ; repeats the statements

条件跳转

如果条件跳转中满足某些指定条件,则控制流将转移到目标指令。根据条件和数据,有许多条件跳转指令。

以下是用于算术运算的有符号数据的条件跳转指令 −

指令 描述 测试的标志
JE/JZ 跳转相等或跳转零 ZF
JNE/JNZ 跳转不相等或跳转不零 ZF
JG/JNLE 跳大于或跳不小于/相等 OF,SF,ZF
JGE/JNL 跳大于/相等或跳不小于 OF,SF
JL/JNGE 跳小于或跳不大于/相等 OF,SF
JLE/JNG 跳小于/相等或跳不大于 OF,SF, ZF

以下是用于逻辑运算 − 的无符号数据的条件跳转指令

指令 描述 测试标志
JE/JZ 跳转相等或跳转零 ZF
JNE/JNZ 跳转不相等或跳转不零 ZF
JA/JNBE 跳转至上方或跳转至不低于/等于 CF,ZF
JAE/JNB 跳转至上方/等于或跳转至不低于 CF
JB/JNAE 跳转至下方或跳转至不高于/等于 CF
JBE/JNA 跳转至下方/等于或跳转至不高于 AF,CF

以下条件跳转指令有特殊用途并检查标志的值 −

指令 描述 测试的标志
JXCZ 如果 CX 为零则跳转
JC 如果有进位则跳转 CF
JNC 如果没有进位则跳转 CF
JO 如果有进位则跳转溢出 OF
JNO 无溢出则跳转 OF
JP/JPE 跳转奇偶校验或跳转奇偶校验 PF
JNP/JPO 跳转无奇偶校验或跳转奇偶校验 PF
JS 跳转符号(负值) SF
JNS 跳转无符号(正值值) SF

J<condition> 指令集 − 的语法

例如,

CMP	AL, BL
JE	EQUAL
CMP	AL, BH
JE	EQUAL
CMP	AL, CL
JE	EQUAL
NON_EQUAL: ...
EQUAL: ...

示例

以下程序显示三个变量中最大的一个。这些变量是两位数变量。三个变量 num1、num2 和 num3 的值分别为 47、22 和 31−

section	.text
   global _start         ;must be declared for using gcc

_start:	                 ;tell linker entry point
   mov   ecx, [num1]
   cmp   ecx, [num2]
   jg    check_third_num
   mov   ecx, [num2]
   
	check_third_num:

   cmp   ecx, [num3]
   jg    _exit
   mov   ecx, [num3]
   
	_exit:
   
   mov   [largest], ecx
   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,largest
   mov   edx, 2
   mov   ebx,1	;file descriptor (stdout)
   mov   eax,4	;system call number (sys_write)
   int   0x80	;call kernel
    
   mov   eax, 1
   int   80h

section	.data
   
   msg db "The largest digit is: ", 0xA,0xD 
   len equ $- msg 
   num1 dd '47'
   num2 dd '22'
   num3 dd '31'

segment .bss
   largest resb 2  

当上述代码被编译并执行时,它会产生以下结果 −

The largest digit is: 
47

汇编 - 循环

JMP 指令可用于实现循环。例如,以下代码片段可用于执行循环体 10 次。

MOV	CL, 10
L1:
<LOOP-BODY>
DEC	CL
JNZ	L1

然而,处理器指令集包括一组用于实现迭代的循环指令。基本 LOOP 指令具有以下语法 −

LOOP 	label

其中,label 是目标标签,用于标识目标指令,就像在跳转指令中一样。LOOP 指令假定 ECX 寄存器包含循环计数。执行循环指令时,ECX 寄存器会递减,控制会跳转到目标标签,直到 ECX 寄存器值(即计数器)达到零值。

上述代码片段可以写成 −

mov ECX,10
l1:
<loop body>
loop l1

示例

以下程序在屏幕上打印数字 1 到 9 −

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   mov ecx,10
   mov eax, '1'
	
l1:
   mov [num], eax
   mov eax, 4
   mov ebx, 1
   push ecx
	
   mov ecx, num        
   mov edx, 1        
   int 0x80
	
   mov eax, [num]
   sub eax, '0'
   inc eax
   add eax, '0'
   pop ecx
   loop l1
	
   mov eax,1             ;system call number (sys_exit)
   int 0x80              ;call kernel
section	.bss
num resb 1

当上述代码被编译并执行时,它会产生以下结果 −

123456789:

汇编 - 数字

数值数据通常以二进制表示。算术指令对二进制数据进行操作。当数字显示在屏幕上或从键盘输入时,它们为 ASCII 形式。

到目前为止,我们已将 ASCII 形式的输入数据转换为二进制以进行算术计算,并将结果转换回二进制。以下代码显示了此过程 −

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

但是,这种转换会产生开销,而汇编语言编程允许以二进制形式更高效地处理数字。十进制数可以用两种形式表示 −

  • ASCII 形式
  • BCD 或二进制编码十进制形式

ASCII 表示

在 ASCII 表示中,十进制数存储为 ASCII 字符串。例如,十进制值 1234 存储为 −

31 32 33 34H

其中,31H 是 1 的 ASCII 值,32H 是 2 的 ASCII 值,依此类推。有四条指令用于处理 ASCII 表示的数字 −

  • AAA − 加法后 ASCII 调整

  • AAS − 减法后 ASCII 调整

  • AAM − 乘法后 ASCII 调整

  • AAD − 除法前 ASCII 调整

这些指令不接受任何操作数,并假设所需操作数位于 AL 寄存器中。

以下示例使用 AAS 指令来演示该概念 −

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   sub     ah, ah
   mov     al, '9'
   sub     al, '3'
   aas
   or      al, 30h
   mov     [res], ax
	
   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,1	        ;message length
   mov	ecx,res	        ;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 'The Result is:',0xa	
len equ $ - msg			
section .bss
res resb 1  

当上述代码被编译并执行时,它会产生以下结果 −

The Result is:
6

BCD 表示法

BCD 表示法有两种类型 −

  • 非压缩 BCD 表示法
  • 压缩 BCD 表示法

在非压缩 BCD 表示法中,每个字节存储十进制数字的二进制等价值。例如,数字 1234 存储为 −

01 02 03 04H

有两条指令用于处理这些数字 −

  • AAM − 乘法后 ASCII 调整

  • AAD − 除法前的 ASCII 调整

四个 ASCII 调整指令 AAA、AAS、AAM 和 AAD 也可以与非压缩 BCD 表示一起使用。在压缩 BCD 表示中,每个数字使用四个位存储。两个十进制数字被压缩成一个字节。例如,数字 1234 存储为 −

12 34H

有两条指令用于处理这些数字 −

  • DAA − 加法后的十进制调整

  • DAS − 减法后的十进制调整

压缩 BCD 表示不支持乘法和除法。

示例

以下程序将两个 5 位十进制数相加并显示总和。它使用上述概念 −

section	.text
   global _start        ;must be declared for using gcc

_start:	                ;tell linker entry point

   mov     esi, 4       ;pointing to the rightmost digit
   mov     ecx, 5       ;num of digits
   clc
add_loop:  
   mov 	al, [num1 + esi]
   adc 	al, [num2 + esi]
   aaa
   pushf
   or 	al, 30h
   popf
	
   mov	[sum + esi], al
   dec	esi
   loop	add_loop
	
   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,5	        ;message length
   mov	ecx,sum	        ;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 'The Sum is:',0xa	
len equ $ - msg			
num1 db '12345'
num2 db '23456'
sum db '     '

当上述代码被编译并执行时,它会产生以下结果 −

The Sum is:
35801

汇编 - 字符串

我们在前面的例子中已经使用了可变长度字符串。可变长度字符串可以包含任意数量的字符。通常,我们通过以下两种方式之一指定字符串的长度 −

  • 明确存储字符串长度
  • 使用标记字符

我们可以通过使用表示位置计数器当前值的 $ 位置计数器符号来明确存储字符串长度。在以下示例中 −

msg  db  'Hello, world!',0xa ;our dear string
len  equ  $ - msg            ;length of our dear string

$ 指向字符串变量 msg 的最后一个字符后的字节。因此,$-msg 给出了字符串的长度。我们也可以这样写

msg db 'Hello, world!',0xa ;our dear string
len equ 13                 ;length of our dear string

或者,您可以存储带有尾部标记字符的字符串来分隔字符串,而不是明确存储字符串长度。标记字符应该是字符串中不出现的特殊字符。

例如 −

message DB 'I am loving it!', 0

字符串指令

每个字符串指令可能需要一个源操作数、一个目标操作数或两者兼而有之。对于 32 位段,字符串指令分别使用 ESI 和 EDI 寄存器指向源操作数和目标操作数。

但是,对于 16 位段,SI 和 DI 寄存器分别用于指向源和目标。

有五条用于处理字符串的基本指令。它们是 −

  • MOVS − 此指令将 1 个字节、字或双字的数据从内存位置移动到另一个位置。

  • LODS − 此指令从内存加载。如果操作数为一个字节,则将其加载到 AL 寄存器中;如果操作数为一个字,则将其加载到 AX 寄存器中;如果操作数为一个字,则将其加载到 EAX 寄存器中。

  • STOS − 该指令将数据从寄存器(AL、AX 或 EAX)存储到内存中。

  • CMPS − 该指令比较内存中的两个数据项。数据可以是字节大小、字或双字。

  • SCAS −此指令将寄存器(AL、AX 或 EAX)的内容与内存中某项的内容进行比较。

上述每条指令都有字节、字和双字版本,字符串指令可以使用重复前缀重复。

这些指令使用 ES:DI 和 DS:SI 寄存器对,其中 DI 和 SI 寄存器包含指向存储在内存中的字节的有效偏移地址。SI 通常与 DS(数据段)相关联,而 DI 始终与 ES(额外段)相关联。

DS:SI(或 ESI)和 ES:DI(或 EDI)寄存器分别指向源操作数和目标操作数。假定源操作数位于内存中的 DS:SI(或 ESI),而目标操作数位于内存中的 ES:DI(或 EDI)。

对于 16 位地址,使用 SI 和 DI 寄存器,对于 32 位地址,使用 ESI 和 EDI 寄存器。

下表提供了各种版本的字符串指令和假定的操作数空间。

基本指令 操作数位于 字节操作 字操作 双字操作
MOVS ES:DI, DS:SI MOVSB MOVSW MOVSD
LODS AX, DS:SI LODSB LODSW LODSD
STOS ES:DI, AX STOSB STOSW STOSD
CMPS DS:SI, ES: DI CMPSB CMPSW CMPSD
SCAS ES:DI, AX SCASB SCASW SCASD

重复前缀

REP 前缀在字符串指令(例如 REP MOVSB)之前设置时,会根据放置在 CX 寄存器中的计数器重复执行该指令。REP 执行该指令,将 CX 减 1,并检查 CX 是否为零。它会重复执行该指令,直到 CX 为零。

方向标志 (DF) 确定操作的方向。

  • 使用 CLD(清除方向标志,DF = 0)使操作从左到右。
  • 使用 STD(设置方向标志,DF = 1)使操作从右到左。

REP 前缀还有以下变体:

  • REP:无条件重复。它会重复该操作,直到 CX 为零。

  • REPE 或 REPZ:它是条件重复。当零标志指示相等/零时,它会重复该操作。当 ZF 指示不相等/零或 CX 为零时,它会停止。

  • REPNE 或 REPNZ:它也是条件重复。当零标志指示不相等/零时,它会重复该操作。当 ZF 指示相等/零或 CX 递减为零时,它会停止。

汇编 - 数组

我们已经讨论过,汇编程序的数据定义指令用于为变量分配存储空间。变量也可以用某个特定值初始化。初始化值可以以十六进制、十进制或二进制形式指定。

例如,我们可以用以下任一方式定义单词变量"months" −

MONTHS	DW	12
MONTHS	DW	0CH
MONTHS	DW	0110B

数据定义指令也可用于定义一维数组。让我们定义一个一维数字数组。

NUMBERS DW 34, 45, 56, 67, 75, 89

上述定义声明了一个由六个字组成的数组,每个字都用数字 34、45、56、67、75、89 初始化。这分配了 2x6 = 12 字节的连续内存空间。第一个数字的符号地址将是 NUMBERS,第二个数字的符号地址将是 NUMBERS + 2,依此类推。

让我们再举一个例子。您可以定义一个名为 inventory 的大小为 8 的数组,并将所有值初始化为零,如下所示 −

INVENTORY   DW  0
            DW  0
            DW  0
            DW  0
            DW  0
            DW  0
            DW  0
            DW  0

可以缩写为 −

INVENTORY DW 0, 0 , 0 , 0 , 0 , 0 , 0 , 0

TIMES 指令还可用于将多个值初始化为同一值。使用 TIMES,INVENTORY 数组可以定义为:

INVENTORY TIMES 8 DW 0

示例

以下示例通过定义一个 3 元素数组 x 来演示上述概念,该数组存储三个值:2、3 和 4。它将数组中的值相加并显示总和 9 −

section	.text
   global _start   ;must be declared for linker (ld)
	
_start:	
 		
   mov  eax,3      ;number bytes to be summed 
   mov  ebx,0      ;EBX will store the sum
   mov  ecx, x     ;ECX will point to the current element to be summed

top:  add  ebx, [ecx]

   add  ecx,1      ;move pointer to next element
   dec  eax        ;decrement counter
   jnz  top        ;if counter not 0, then loop again

done: 

   add   ebx, '0'
   mov  [sum], ebx ;done, store result in "sum"

display:

   mov  edx,1      ;message length
   mov  ecx, sum   ;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
global x
x:    
   db  2
   db  4
   db  3

sum: 
   db  0

当上述代码被编译并执行时,它会产生以下结果 −

9

汇编 - 程序

程序或子程序在汇编语言中非常重要,因为汇编语言程序往往很大。程序由名称标识。在此名称之后,描述了执行明确定义的工作的程序主体。程序的结束由 return 语句表示。

语法

以下是定义程序的语法 −

proc_name:
   procedure body
   ...
   ret

使用 CALL 指令从另一个函数调用该过程。CALL 指令应将被调用过程的名称作为参数,如下所示 −

CALL proc_name

被调用过程使用 RET 指令将控制权返回给调用过程。

示例

让我们编写一个名为 sum 的非常简单的过程,它将存储在 ECX 和 EDX 寄存器中的变量相加,并在 EAX 寄存器中返回总和 −

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   mov	ecx,'4'
   sub     ecx, '0'
	
   mov 	edx, '5'
   sub     edx, '0'
	
   call    sum          ;call sum procedure
   mov 	[res], 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, 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
sum:
   mov     eax, ecx
   add     eax, edx
   add     eax, '0'
   ret
	
section .data
msg db "The sum is:", 0xA,0xD 
len equ $- msg   

segment .bss
res resb 1

当上述代码被编译并执行时,它会产生以下结果 −

The sum is:
9

堆栈数据结构

堆栈是内存中的一种类似数组的数据结构,其中的数据可以在称为堆栈"顶部"的位置存储和删除。需要存储的数据被"推送"到堆栈中,需要检索的数据从堆栈中"弹出"。堆栈是一种 LIFO 数据结构,即先存储的数据最后检索。

汇编语言为堆栈操作提供了两条指令:PUSH 和 POP。这些指令的语法如下 −

PUSH    operand
POP     address/register

堆栈段中保留的内存空间用于实现堆栈。寄存器 SS 和 ESP(或 SP)用于实现堆栈。堆栈顶部指向插入堆栈的最后一个数据项,由 SS:ESP 寄存器指向,其中 SS 寄存器指向堆栈段的开头,SP(或 ESP)给出堆栈段的偏移量。

堆栈实现具有以下特点 −

  • 只有 双字 可以保存到堆栈中,而不能保存字节。

  • 堆栈以相反的方向增长,即朝着较低的内存地址增长

  • 堆栈顶部指向插入堆栈的最后一个项目;它指向插入的最后一个字的低字节。

正如我们讨论的那样,在使用寄存器的值之前将其存储在堆栈中;可以按照以下方式完成 −

; Save the AX and BX registers in the stack
PUSH    AX
PUSH    BX

; Use the registers for other purpose
MOV	AX, VALUE1
MOV 	BX, VALUE2
...
MOV 	VALUE1, AX
MOV	VALUE2, BX

; Restore the original values
POP	BX
POP	AX

示例

以下程序显示整个 ASCII 字符集。主程序调用名为 display 的过程,该过程显示 ASCII 字符集。

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   call    display
   mov	eax,1	        ;system call number (sys_exit)
   int	0x80	        ;call kernel
	
display:
   mov    ecx, 256
	
next:
   push    ecx
   mov     eax, 4
   mov     ebx, 1
   mov     ecx, achar
   mov     edx, 1
   int     80h
	
   pop     ecx	
   mov	dx, [achar]
   cmp	byte [achar], 0dh
   inc	byte [achar]
   loop    next
   ret
	
section .data
achar db '0'  

当上述代码被编译并执行时,它会产生以下结果 −

0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
...
...

汇编 - 递归

递归过程是调用自身的过程。有两种递归:直接递归和间接递归。在直接递归中,过程调用自身;在间接递归中,第一个过程调用第二个过程,后者又调用第一个过程。

递归可以在许多数学算法中观察到。例如,考虑计算数字阶乘的情况。数字阶乘由以下方程给出 −

Fact (n) = n * fact (n-1) for n > 0

例如:5 的阶乘为 1 x 2 x 3 x 4 x 5 = 5 x 4 的阶乘,这可以很好地展示递归过程。每个递归算法都必须有一个结束条件,即当满足条件时,程序的递归调用应停止。对于阶乘算法,当 n 为 0 时达到结束条件。

以下程序显示了如何用汇编语言实现阶乘 n。为了使程序简单,我们将计算阶乘 3。

section	.text
   global _start         ;must be declared for using gcc
	
_start:                  ;tell linker entry point

   mov bx, 3             ;for calculating factorial 3
   call  proc_fact
   add   ax, 30h
   mov  [fact], ax
    
   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,1            ;message length
   mov	  ecx,fact       ;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
	
proc_fact:
   cmp   bl, 1
   jg    do_calculation
   mov   ax, 1
   ret
	
do_calculation:
   dec   bl
   call  proc_fact
   inc   bl
   mul   bl        ;ax = al * bl
   ret

section	.data
msg db 'Factorial 3 is:',0xa	
len equ $ - msg			

section .bss
fact resb 1

当上述代码被编译并执行时,它会产生以下结果 −

Factorial 3 is:
6

汇编 - 宏

编写宏是确保汇编语言模块化编程的另一种方法。

  • 宏是一系列指令,由名称指定,可在程序中的任何位置使用。

  • 在 NASM 中,宏使用 %macro%endmacro 指令定义。

  • 宏以 %macro 指令开头,以 %endmacro 指令结尾。

宏定义的语法 −

%macro macro_name number_of_params
<macro body>
%endmacro

其中,number_of_params指定参数的数量,macro_name指定宏的名称。

使用宏名称以及必要的参数来调用宏。当您需要在程序中多次使用某些指令序列时,您可以将这些指令放在宏中并使用它,而不必一直编写指令。

例如,程序的一个非常常见的需求是在屏幕上写入一串字符。要显示一串字符,您需要以下指令序列 −

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

在上面显示字符串的例子中,寄存器 EAX、EBX、ECX 和 EDX 已被 INT 80H 函数调用使用。因此,每次需要在屏幕上显示时,都需要将这些寄存器保存在堆栈上,调用 INT 80H,然后从堆栈中恢复寄存器的原始值。因此,编写两个宏来保存和恢复数据可能会很有用。

我们已经观察到,一些指令(如 IMUL、IDIV、INT 等)需要将一些信息存储在某些特定寄存器中,甚至返回某些特定寄存器中的值。如果程序已经使用这些寄存器来保存重要数据,那么这些寄存器中的现有数据应该保存在堆栈中,并在执行指令后恢复。

示例

以下示例显示定义和使用宏 −

; A macro with two parameters
; Implements the write system call
   %macro write_string 2 
      mov   eax, 4
      mov   ebx, 1
      mov   ecx, %1
      mov   edx, %2
      int   80h
   %endmacro
 
section	.text
   global _start            ;must be declared for using gcc
	
_start:                     ;tell linker entry point
   write_string msg1, len1               
   write_string msg2, len2    
   write_string msg3, len3  
	
   mov eax,1                ;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!

汇编 - 文件管理

系统将任何输入或输出数据视为字节流。有三个标准文件流 −

  • 标准输入 (stdin),
  • 标准输出 (stdout),和
  • 标准错误 (stderr)。

文件描述符

文件描述符是一个 16 位整数,作为文件 ID 分配给文件。创建新文件或打开现有文件时,文件描述符用于访问文件。

标准文件流的文件描述符 - stdin、stdoutstderr 分别为 0、1 和 2。

文件指针

文件指针以字节为单位指定文件中后续读/写操作的位置。每个文件都被视为一个字节序列。每个打开的文件都与一个文件指针相关联,该文件指针指定相对于文件开头的偏移量(以字节为单位)。打开文件时,文件指针设置为零。

文件处理系统调用

下表简要介绍了与文件处理相关的系统调用 −

%eax Name %ebx %ecx %edx
2 sys_fork struct pt_regs - -
3 sys_read unsigned int char * size_t
4 sys_write unsigned int const char * size_t
5 sys_open const char * int int
6 sys_close unsigned int - -
8 sys_creat const char * int -
19 sys_lseek unsigned int off_t unsigned int

使用系统调用所需的步骤与我们之前讨论的相同 −

  • 将系统调用号放入 EAX 寄存器中。
  • 将系统调用的参数存储在寄存器 EBX、ECX 等中。
  • 调用相关中断 (80h)。
  • 结果通常返回在 EAX 寄存器中。

创建和打开文件

要创建和打开文件,请执行以下任务 −

  • 将系统调用 sys_creat() 编号 8 放入 EAX 寄存器中。
  • 将文件名放入 EBX 寄存器中。
  • 将文件权限放入 ECX 寄存器中。

系统调用返回文件描述符所创建文件的文件描述符存储在 EAX 寄存器中,如果发生错误,错误代码存储在 EAX 寄存器中。

打开现有文件

要打开现有文件,请执行以下任务 −

  • 将系统调用 sys_open() 编号 5 放入 EAX 寄存器中。
  • 将文件名放入 EBX 寄存器中。
  • 将文件访问模式放入 ECX 寄存器中。
  • 将文件权限放入 EDX 寄存器中。

系统调用将所创建文件的文件描述符返回到 EAX 寄存器中,如果发生错误,错误代码存储在 EAX 寄存器中。

在文件访问模式中,最常用的是:只读 (0)、只写 (1) 和读写 (2)。

读取从文件中读取

要从文件中读取,请执行以下任务 −

  • 将系统调用 sys_read() 编号 3 放入 EAX 寄存器中。

  • 将文件描述符放入 EBX 寄存器中。

  • 将指向输入缓冲区的指针放入 ECX 寄存器中。

  • 将缓冲区大小(即要读取的字节数)放入 EDX 寄存器中。

系统调用返回 EAX 寄存器中读取的字节数,如果发生错误,错误代码位于 EAX 寄存器中。

写入文件

要写入文件,请执行以下任务 −

  • 将系统调用sys_write() 编号 4,放在 EAX 寄存器中。

  • 将文件描述符放入 EBX 寄存器中。

  • 将指向输出缓冲区的指针放入 ECX 寄存器中。

  • 将缓冲区大小(即要写入的字节数)放入 EDX 寄存器中。

系统调用返回 EAX 寄存器中写入的实际字节数,如果发生错误,错误代码位于 EAX 寄存器中。

关闭文件

要关闭文件,请执行以下任务 −

  • 将系统调用 sys_close() 编号 6 放入 EAX 寄存器中。
  • 将文件描述符放入 EBX 寄存器中。

如果发生错误,系统调用将返回 EAX 寄存器中的错误代码。

更新文件

要更新文件,请执行以下任务 −

  • 将系统调用 sys_lseek () 编号 19 放入 EAX 寄存器中。
  • 将文件描述符放入 EBX 寄存器中。
  • 将偏移值放入 ECX 寄存器中。
  • 将偏移的参考位置放入 EDX 寄存器中。

参考位置可以是:

  • 文件开头 - 值 0
  • 当前位置 - 值 1
  • 文件结尾 - 值 2

如果出现错误,系统调用将返回 EAX 寄存器中的错误代码。

示例

以下程序创建并打开一个名为 myfile.txt 的文件,并在该文件中写入文本"Welcome to Tutorials Point"。接下来,程序从文件中读取数据并将数据存储到名为 info 的缓冲区中。最后,它显示存储在 info 中的文本。

section	.text
   global _start         ;must be declared for using gcc
	
_start:                  ;tell linker entry point
   ;create the file
   mov  eax, 8
   mov  ebx, file_name
   mov  ecx, 0777        ;read, write and execute by all
   int  0x80             ;call kernel
	
   mov [fd_out], eax
    
   ; write into the file
   mov	edx,len          ;number of bytes
   mov	ecx, msg         ;message to write
   mov	ebx, [fd_out]    ;file descriptor 
   mov	eax,4            ;system call number (sys_write)
   int	0x80             ;call kernel
	
   ; close the file
   mov eax, 6
   mov ebx, [fd_out]
    
   ; write the message indicating end of file write
   mov eax, 4
   mov ebx, 1
   mov ecx, msg_done
   mov edx, len_done
   int  0x80
    
   ;open the file for reading
   mov eax, 5
   mov ebx, file_name
   mov ecx, 0             ;for read only access
   mov edx, 0777          ;read, write and execute by all
   int  0x80
	
   mov  [fd_in], eax
    
   ;read from file
   mov eax, 3
   mov ebx, [fd_in]
   mov ecx, info
   mov edx, 26
   int 0x80
    
   ; close the file
   mov eax, 6
   mov ebx, [fd_in]
   int  0x80 
	
   ; print the info 
   mov eax, 4
   mov ebx, 1
   mov ecx, info
   mov edx, 26
   int 0x80
       
   mov	eax,1             ;system call number (sys_exit)
   int	0x80              ;call kernel

section	.data
file_name db 'myfile.txt'
msg db 'Welcome to Tutorials Point'
len equ  $-msg

msg_done db 'Written to file', 0xa
len_done equ $-msg_done

section .bss
fd_out resb 1
fd_in  resb 1
info resb  26

当上述代码被编译并执行时,它会产生以下结果 −

Written to file
Welcome to Tutorials Point

汇编 - 内存管理

内核提供sys_brk()系统调用,用于分配内存而无需稍后移动。此调用在内存中紧跟应用程序映像之后分配内存。此系统函数允许您在数据部分中设置最高可用地址。

此系统调用接受一个参数,即需要设置的最高内存地址。此值存储在 EBX 寄存器中。

如果发生任何错误,sys_brk() 将返回 -1 或返回负错误代码本身。以下示例演示了动态内存分配。

示例

以下程序使用 sys_brk() 系统调用分配 16kb 内存 −

section	.text
   global _start         ;must be declared for using gcc
	
_start:	                 ;tell linker entry point

   mov	eax, 45		 ;sys_brk
   xor	ebx, ebx
   int	80h

   add	eax, 16384	 ;number of bytes to be reserved
   mov	ebx, eax
   mov	eax, 45		 ;sys_brk
   int	80h
	
   cmp	eax, 0
   jl	exit	;exit, if error 
   mov	edi, eax	 ;EDI = highest available address
   sub	edi, 4		 ;pointing to the last DWORD  
   mov	ecx, 4096	 ;number of DWORDs allocated
   xor	eax, eax	 ;clear eax
   std			 ;backward
   rep	stosd            ;repete for entire allocated area
   cld			 ;put DF flag to normal state
	
   mov	eax, 4
   mov	ebx, 1
   mov	ecx, msg
   mov	edx, len
   int	80h		 ;print a message

exit:
   mov	eax, 1
   xor	ebx, ebx
   int	80h
	
section	.data
msg    	db	"Allocated 16 kb of memory!", 10
len     equ	$ - msg

当上述代码被编译并执行时,它会产生以下结果 −

Allocated 16 kb of memory!