组装 - 快速指南


装配 - 简介

什么是汇编语言?

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

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

处理器只能理解机器语言指令,即由 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

  • cdnasm-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 环境变量中设置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关键字替换为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 中。

算术1

2

当两个单字值相乘时 -

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

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

算术2

3

当两个双字值相乘时 -

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

算术3

例子

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 寄存器。

算术4

2

当除数为 1 个字时 -

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

算术5

3

当除数为双字时 -

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

算术6

例子

以下示例将 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 不是