汇编语言 - 快速指南



汇编语言 - 简介

什么是汇编语言?

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

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

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

汇编语言的优点

了解汇编语言使人能够了解:

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

使用汇编语言的其他优点包括:

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

  • 它允许以更简单的方式执行硬件特定的复杂任务;

  • 它适用于时间关键型任务;

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

PC硬件的基本特征

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

计算机存储的基本单位是位;它可以是开(1)或关(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 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

最后一位的溢出将丢失。

内存中数据的寻址

处理器控制指令执行的过程称为取指-译码-执行周期执行周期。它包含三个连续的步骤:

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

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

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

Introduction

x:内存地址

当处理器从内存获取数值数据到寄存器时,它会再次反转字节。有两种内存地址:

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

  • 段地址(或偏移量) - 内存段的起始地址以及偏移值。

汇编语言 - 环境搭建

本地环境设置

汇编语言依赖于处理器的指令集和体系结构。在本教程中,我们关注的是英特尔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,请执行以下步骤:

  • 查看The Netwide Assembler (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以将nasm和ndisasm安装到/usr/local/bin中,并安装手册页。

这应该会在您的系统上安装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中编译和链接汇编程序

确保你已将nasmld二进制文件的路径设置到你的PATH环境变量中。现在,按照以下步骤编译和链接上述程序:

  • 使用文本编辑器输入上述代码,并将其保存为hello.asm。

  • 确保你当前目录与保存hello.asm文件的目录相同。

  • 要汇编程序,请键入nasm -f elf hello.asm

  • 如果存在任何错误,在此阶段将提示你。否则,将创建名为hello.o的程序目标文件。

  • 要链接目标文件并创建名为hello的可执行文件,请键入ld -m elf_i386 -s -o hello hello.o

  • 键入./hello执行程序

如果一切正确,它将在屏幕上显示“Hello, world!”。

汇编语言 - 内存段

我们已经讨论了汇编程序的三个部分。这些部分也代表不同的内存段。

有趣的是,如果你用segment替换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架构中有十个32位和六个16位处理器寄存器。这些寄存器分为三类:

  • 通用寄存器;
  • 控制寄存器;以及
  • 段寄存器。

通用寄存器进一步细分为以下几组:

  • 数据寄存器;
  • 指针寄存器;以及
  • 索引寄存器。

数据寄存器

四个32位数据寄存器用于算术、逻辑和其他运算。这四个32位寄存器可以使用三种方式:

  • 作为完整的32位数据寄存器:EAX、EBX、ECX、EDX。

  • 32位寄存器的低半部分可以用作四个16位数据寄存器:AX、BX、CX和DX。

  • 上述四个16位寄存器的低半部分和高半部分可以用作八个8位数据寄存器:AH、AL、BH、BL、CH、CL、DH和DL。

Data Registers

其中一些数据寄存器在算术运算中具有特定用途。

AX是主累加器;它用于输入/输出和大多数算术指令。例如,在乘法运算中,根据操作数的大小,一个操作数存储在EAX或AX或AL寄存器中。

BX称为基址寄存器,因为它可用于索引寻址。

CX称为计数寄存器,因为ECX、CX寄存器在迭代运算中存储循环计数。

DX称为数据寄存器。它也用于输入/输出操作。它也与AX寄存器一起用于涉及大值的乘法和除法运算。

指针寄存器

指针寄存器是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组合用作特殊寻址的基址寄存器。

Pointer Registers

索引寄存器

32位索引寄存器ESI和EDI及其16位最右半部分SI和DI用于索引寻址,有时也用于加法和减法。有两组索引指针:

  • 源索引(SI) - 它用作字符串操作的源索引。

  • 目标索引(DI) - 它用作字符串操作的目标索引。

Index Registers

控制寄存器

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
位号 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 名称 %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汇编指令用于分配存储空间。它可以用于保留和初始化一个或多个字节。

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

已初始化数据的存储分配语句的语法如下:

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

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

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位二进制等效值并存储为十六进制数。

  • 处理器使用小端字节序。

  • 负数转换为其二进制补码表示。

  • 短浮点数和长浮点数分别使用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指令允许对相同值进行多次初始化。例如,可以使用以下语句定义大小为9的名为marks的数组并将其初始化为零:

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

%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

操作数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

操作数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

在这两种情况下,被乘数都在累加器中,取决于被乘数和乘数的大小,生成的乘积也存储在两个寄存器中,取决于操作数的大小。下一节将解释三种不同情况下的MUL指令:

序号 场景
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位操作数。该操作影响所有六个状态标志。下一节将解释三种不同操作数大小的除法情况:

序号 场景
1

当除数为1字节时:

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

Arithmetic4

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布尔逻辑指令,这些指令根据程序的需要测试、设置和清除位。

这些指令的格式:

序号 指令 格式
1 AND AND operand1, operand2
2 OR OR operand1, operand2
3 XOR XOR operand1, operand2
4 TEST TEST operand1, operand2
5 NOT NOT operand1

在所有情况下,第一个操作数都可以位于寄存器或内存中。第二个操作数可以位于寄存器/内存中,也可以是立即数(常量)值。但是,内存到内存的操作是不可能的。这些指令比较或匹配操作数的位,并设置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运算来支持逻辑表达式。如果来自一个或两个操作数的匹配位为一,则按位OR运算符返回1。如果两个位都是零,则返回0。

例如:

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

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

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

示例

下面的例子演示了OR指令。让我们分别在AL和BL寄存器中存储值5和3,然后指令:

OR AL, BL

应在AL寄存器中存储7:

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

将操作数与自身进行**XOR**运算会将操作数更改为**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

汇编语言 - 条件语句

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

序号 条件指令
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码形式的输入数据转换为二进制形式进行算术运算,并将结果转换回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

在上例显示字符字符串中,INT 80H 函数调用使用了寄存器 EAX、EBX、ECX 和 EDX。因此,每次需要在屏幕上显示内容时,都需要将这些寄存器保存在堆栈中,调用 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 名称 %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!
广告