基本语法
汇编程序可以分为三个段
- data段
- bss段
- text段
data段
数据段被用于声明初始化的数据或常数。此数据在运行时不会更改。可以在段中声明各种常用值,文件名或缓冲区大小等。
1 | section .data |
bss段
在bss声明变量。
1 | section .bss |
text段
代码段被用于保持实际的代码。该段必须以全集声明_start开头,该声明告诉内核程序从何处开始执行。
1 | section .test |
注释
汇编语言注释以分号(;)开头。它可以包含任何可打印字符,包括空格。它可以单独出现在一行上。
1 | ;该程序在屏幕上显示一条信息 |
或者,与指令在同一行上。
1 | add eax, ebx ;加上ebx的值到eax |
汇编语言声明
汇编语言程序包含三种类型的语句
- 可执行指令或说明
- 汇编程序指令或伪操作
- 宏
可执行指令或简单的指令告诉处理器做什么。每个指令由一个操作码组成。每个可执行指令生成一个机器语言指令。该汇编指令或位操作讲述装配过程中的各方面的汇编。这些是不可执行的,不会生成机器语言指令。宏是一种代码替换机制。
汇编语言语句的语法
汇编语言语句每行输入一个语句。每个语句遵循一下格式
1 | [label] mnemonic [operands] [;comment] |
基本命令包括两段,第一段是要执行的指令(或助记符)的名称,第二段是命令的操作数或参数。以下是一些典型汇编语句的示例
1 | inc count ; 增加内存变量count |
汇编语言的Hello World 程序
1 | section .text |
在NASM中编译和链接程序
1 | nasm -f elf hello.asm |
汇编语言 内存段
内存段
如果将section 关键字替换为segment,也会得到相同的结果。
1 | segment .text ;code segment |
内存段
分段存储器模型将系统存储器分为独立的分段组,这些分段由位于分段寄存器中的指针引用。每个段用于包含特定类型的数据。一个段用于包含指令代码,另一段用于存储数据元素,第三段保留程序堆栈。
- 数据段 由.data段和.bss段表示。.data节用于声明存储区,在该存储区中为程序存储了数据元素。声明数据元素后,无法扩展此部分,并且在整个程序中他保持静态。.bss部分也是一个静态内存部分,它包含缓冲区,供稍后在程序中声明的数据使用。这个缓冲区内存是零填充的
- 代码段 由.text部分表示。这在内存中定义了一个存储指令代码的区域。这也是一个固定区域
- 堆栈 该段包含传递给程序中的函数和过程的数据值
寄存器
寄存器
处理器操作主要涉及到处理数据。该数据可以存储在存储器中并从其访问。从存储器中读取数据中将数据存储到存储器中会减慢处理器的速度,因为这涉及到通过控制总线发送数据请求并进入存储器存储单元并通过同意通道获取数据的复杂过程。
为了加速处理器的运行,处理器包括一些内存处理器存储位置,称为寄存器。寄存器存储要处理的数据元素,而不必访问存储器。处理器芯片中内置了数量有限的寄存器
处理器寄存器
IA-32体系结构中有10个32位和6个16为处理器寄存器。寄存器分三类
- 通用寄存器
- 控制寄存器
- 段寄存器
通用寄存器分为以下几类
- 数据寄存器
- 指针寄存器
- 索引寄存器
// todo 有个寄存器不知道叫啥
四个32位数据寄存器用于算术,用于逻辑和其他操作。
完整的32位寄存器:EAX,EBX,ECX,EDX。下半部分的32位寄存器可用作四个16位寄存器:AX,BX,CX,DX。四个16位寄存器的上半部分和下半部分可以用作八个8位寄存器:AH,BH,CH,DH,AL,BL,CL,DL
- AX主要的累加器。用于输出/输入和绝大多数算术指令
- BX被称作基址寄存器,因为它可以用于索引寻址
- CX被称作计数寄存器,因为ECX,CX寄存器在迭代操作中存储循环计数
- DX被称作数据寄存器。它也用于输入/输出操作。他还与AX寄存器以及DX一起使用,用于涉及大数值的乘法和除法运算
指针寄存器
指针寄存器是32位EIP,ESP和EBP寄存器以及相应的16位右部分IP,SP和BP。指针寄存器分为三类
- 指令指针(IP)16位IP寄存器存储要执行的下一条指令的偏移地址。与CS寄存器
- 堆栈指针(SP)16位SP寄存器提供程序堆栈内的偏移值。与SS寄存器(SS:SP)关联的SP是指程序堆栈中数据或地址的当前位置。
- 基本指针(BP)16位BP寄存器主要帮助参考传递给子进程的参数变量。SS寄存器中的地址与BP中的偏移量相结合,以获取参数的位置。BP也可以与DI和SI组合用作特殊寻址的基址寄存器。
索引寄存器
32位索引寄存器ESI和EDI及最右边的16位部分。SI和DI用于索引寻址,有时用于加法和减法。有两组索引指针
- 源索引(SI)用作字符串操作的源索引
- 目的索引(DI)用作字符串操作的目标索引
控制寄存器
将32位指令指针寄存器的32位标志寄存器组合起来称为控制寄存器。许多指令涉及比较和数学计算,并更改标志的状态,而其他一些条件指令则测试这些状态标志的值,将控制流带到其他位置
通用标志位是:
- 溢出标志(OF)指示有符号算术运算后数据的高阶位(最左位)的溢出。
- 方向标志(DF)它确定向左或向右移动或比较字符串数据的方向。DF值为0时,字符串操作为从左向右的方向;当DF值为1时,字符串操作为从右至左的方向
- 中断标志(IF)确定是否忽略或处理外部中断(如键盘中断输入等)。当值为0时,它禁用外部中断,当值为1时,使能中断
- 陷阱标志(TF)允许在单步模式下设置处理器的操作。我们使用的DEBUG程序设置了陷阱标志,因此我们可以一次逐步执行一条指令。
- 符号标志(SF)显示算术运算结果符号。根据算术运算后数据项的符号设置此标志。该符号由最左位的高位指示。正结果将SF的值清除为0,负结果将其设置为1.
- 零标志(ZF)指示算术或比较运算的结果。非零结果将零标志清零,零结果将其清零
- 辅助进位标志(AF)包含经过算术运算后从位3到位4的进位;用于专业算术。当1字节算术运算引起从第3位到第4位的进位时,将设置AF。
- 奇偶校验标志(PF)指示从算术运算获得的结果中1位的总数。偶数个1位将奇数校验标志位清为0,奇数个1位将将奇偶校验标志清为1。
- 进位标志(CF)在算术运算后,它包含一个高位(最左边)的进位。它还存储移位或旋转操作的最后一位的内容。
段寄存器
段是程序中定义的特定区域,用于包含数据,代码和堆栈。有三个部分
- 代码段(CS) 它包含所有要执行的指令。16位代码段寄存器或CS寄存器存储代码段的起始地址。
- 数据段(DS) 它包含数据、常量和工作区。16位数据段寄存器或DS寄存器存储数据段的起始地址。
- 堆栈段(SS) 它包含数据或过程或子进程的返回地址。它被实现为“堆栈”数据结构。堆栈段寄存器或SS寄存器存储堆栈的起始地址。
除了DS,CS,SS寄存器外,还有其他段寄存器-ES(额外段),FS和GS,他们提供了用于存储数据的其他段。在汇编语言中,程序需要访问存储器的位置。段中的所有存储位置都相对于段的起始地址。段的其实地址可以时16或者16进制的整数,因此,所有此类存储地址中最右边的十六进制数字为0,通常不存储在段寄存器中。段寄存器存储段的起始地址。为了获得数据或指令在段中的确切位置,需要一个偏移值(或者位移)。为了引用段中的任何存储位置,处理器将段寄存器中的段地址与该位置的偏移值进行组合。
1 | section .text |
系统调用
系统调用
是用户空间和内核空间之间接口的API。我们已经使用了系统调用。sys_write和sys_exit分别用于写入屏幕和退出程序
Linux系统调用
可以通过以下步骤使用linux系统调用
- 将系统呼叫号放入EAX寄存器中
- 将参数保存到系统调用中的寄存器EBX、ECX等中
- 调用相关的中断(80h)
- 结果通常在EAX寄存器中返回
用于存储系统调用参数的寄存器有六个,分别是EBX、EDX、ESI、EDI和EBP,这些寄存器采用从EBX寄存器开始的连续参数。如果有六个以上的自变量,则第一个自变量的存储位置将存储在EBX寄存器中。
1 | mov eax,1 |
sys_write的使用
1 | mov ebx,4 |
所有系统调用及其编号都列在/usr/include/asm/unistd.h中
从键盘读取一个数字并将其显示在屏幕上
1 |
|
寻址模式
寻址模式
大多数汇编语言指令都需要处理操作数。操作数地址提供了要处理的数据存储的位置。有些指令不需要操作数,而另一些指令则需要一个,两个或者三个操作数。当一条指令需要两个操作数时,第一个操作数通常时目的地,他在寄存器或存储器位置中包含数据,第二个操作数是源。源包含要传递的数据(立即寻址)或数据的地址(在寄存器或者存储器中)。通常,操作后源数据保持不变。
寻址的三种基本模式是
- 寄存器寻址
- 立即寻址
- 内存寻址
寄存器寻址
在这种寻址模式下,寄存器包含操作数。根据指令,寄存器可以是第一个操作数,第二个操作数或者两者。
1 | mov dx,tax_rate ;寄存器是第一个操作数 |
由于寄存器之间的数据处理不涉及内存,因此可以最快的处理数据
立即寻址
立即数操作数具有常量值或表达式。当具有两个操作数的指令使用立即寻址时,第二个操作数可以是寄存器或者存储器位置,而第二个操作数是立即数。第一个操作数定义数据的长度
1 | BYTE_VALUE DB 150 ;一个字节值被定义 |
直接内存寻址
在内存寻址模式下指定操作数时,通常需要直接访问主存储器,通常时数据段。这种寻址方式导致数据处理变慢。为了找到数据在内存中的确切位置,我们需要短期是地址(通常在DS寄存器中找到)和偏移量。因此偏移量也称为有效地址。在直接寻址模式下,偏移量值直接作为指令的一部分指定,通常由变量名指示。汇编器计算偏移值并维护一个符号表,该表存储程序中使用的所有变量的偏移值。直接存储寻址中,一个操作数引用一个存储器位置,另一个操作数引用一个寄存器
1 | ADD BYTE_VALUE,DL ;将寄存器添加到存储位置 |
直接偏移寻址
该模式使用算术运算符修改地址
1 | BYTE_TABLE DB 14,15,22,45 ;字节表 |
以下操作数将数据从内存中的表访问到寄存器中
1 | mov cl,BYTE_TABLE[2] |
间接内存寻址
此寻址模式利用计算机的Segmeng:Offset寻址功能功能。通常,在方括号内百年好的基址寄存器EBX,EBP(或BX,BP)和索引寄存器(DI,SI)用于内存引用
间接寻址通常用于包含多个元素(如数组)的变量。阵列的其实地址存储在EBX寄存器中。
1 | my_table times 10 DW 0 ; 分配10个字(2个字节),每个字都初始化为0 |
MOV 指令
该指令用于将数据从一个存储空间移动到另一个存储空间。mov指令采用两个操作数。
1 | MOV destination,source |
MOV 指令可能具有以下五种形式之一
1 | MOV 寄存器, 寄存器 |
注意
- MOV操作数中的两个操作数应该具有相同的大小
- 源操作数的值保持不变
以下命令会引起歧义:
1 | mov ebx, [my_table] |
目前不清楚要移动等于110的字节等效值还是等效于字的字符。在这种情况下使用类型说明符时明智的。
类型说明符 | 寻址字节 |
---|---|
BYTE | 1 |
WORD | 2 |
DWORD | 4 |
QWORD | 8 |
TBYTE | 10 |
1 |
|
变量
变量
nasm提供了各种定义指令来为变量保留存储空间。define assembler指令用于分配存储空间。它可以用于保留以及初始化一个或多个字节。
为初始数据分配存储空间
初始化数据的存储分配语句的语法为
1 | [variable-name] define-directive initial-value [,initial-value] |
其中,变量名是每个存储空间的标识符。汇编器为数据段中定义的每个变量名称关联一个偏移值。
define指令有五种基本形式
指令 | 目的 | 存储空间 |
---|---|---|
DB | 定义字节 | 分配一个字节 |
DW | 定义字 | 分配两个字节 |
DD | 定义双字 | 分配四个字节 |
DQ | 定义四字 | 分配八个字节 |
DT | 定义十个字节 | 分配十个字节 |
以下时一些使用define指令的示例
1 | choice db 'y' |
注意
- 字符的每个字节均以十六进制形式存储为ASCII值
- 每个十进制值都将自动转换为其等效的16位二进制数,并以十六进制数形式存储
- 处理器使用小尾数字节顺序
- 负数将转换为其2的补码表示形式
- 短浮点数和长浮点数分别使用32位和64位表示
define指令的使用
1 | section .text |
为未初始化的数据分配存储空间
reserve指令用于为未初始化的数据保留空间。reserve指令采用单个操作数,该操作数指定要保留的空间单位数。每个define指令都有一个相关的reserve指令。
保留指令有五种基本形式
指令 | 目的 |
---|---|
RESB | 保留一个字节 |
RESW | 保留字 |
RESD | 保留双字 |
RESQ | 保留四字 |
REST | 保留十个字 |
多种定义
一个程序中可以有多个数据定义语句。例如
1 | choice DB 'y' |
多重初始化
TIMES 指令允许多次初始化为相同的值。下面语句定义一个大小为9的标记数组并将其初始化为零
1 | marks TIMES 9 DW 0 |
TIMES指令在定义数组和表时很有用
1 | section .text |
常量
常量
nasm提供了几个定义常量的指令。之前我们已经使用了EQU指令,下面将特别讨论几个
- EQU
- %assign
- %define
EQU指令
EQU指令用于定义常量。EQU指令的语法如下
1 | constant_name equ expression |
以上代码段将area定义为200
1 | SYS_EXIT equ 1 |
%assign指令
在%assign指令可以用来定义数字常量像equ指令。该指令允许重新定义。
1 | %assign total 10;定义常量total为10 |
注意
该指令区分大小写
%define指令
在%define指令允许定义数值和字符串常量。类似于#define
1 | %define PTR [EBP+4] |
该指令还允许重新定义,并且区分大小写。
算术指令
INC指令
INC 指令用于将操作数加1.它可以对寄存器或内存中的单个操作数起作用
1 | INC destination |
DEC指令
DEC指令用于将操作数减一。它可以对在寄存器或内存中的单个操作数起作用。
1 | DEC destination |
操作数目的可以是8位,16位或32位操作数
1 | segment .data |
ADD和SUB指令
ADD和SUB指令用于对字节,字和双字大小的二进制数进行简单的加/减,即分别用于添加或减去8位、16位、和32位操作数
1 | ADD/SUB destination, source |
ADD/SUB指令可以用于
- 寄存器 to 寄存器
- 内存 to 寄存器
- 寄存器 to 内存
- 寄存器 to 常量数据
- 内存 to 常量数据
但是,像其他指令一样,使用ADD/SUB指令也无法进行存储器到存储器的操作。ADD或SUB操作设置或清除溢出和进位标志
下面的示例将要求用于输入两位数字,分别将这些数字存储在EAX和EBX寄存器中,将这些值相加,将结果存储在res存储位置中,最后显示显示结果
1 | ; 为什么equ 指令可以不在section .data中 |
MUL/IMUL指令
有两条指令用于将二进制数据相乘。MUL(乘法)指令处理unsign的数据,而IMUL(整数乘法)则处理sign数据。两条指令影响进位和溢出标志
MUL/IMUL指令的语法如下:
1 | MUL/IMUL multiplier |
在这种情况下,被乘数都将在一个累加器中,具体取决于被乘数和乘数的大小,并更具操作数的大小,生成的乘积还将存储在两个寄存器中。以下部分说明了三种不同情况下的MUL指令
情况 | 描述 |
---|---|
当两个字节相乘时 | 被乘数在AL寄存器中,而乘数在存储器或 另一个寄存器中为一个字节。此情况使用AX。 乘积的高8位存储在AH中,低8位存储在AL中。 |
当两个单字相乘时 | 被乘数应位于AX寄存器中,并且乘数是内存 或其他寄存器中的一个字。列如,对于MUL DX之类的指令, 必须将乘数存储在DX中,将被乘数存储在AX中。结果乘积是一个双字。 将需要两个寄存器。高阶(最左侧)部分存储在DX中,而低阶(最右侧) 部分存储在AX中 |
当两个双字值相乘时 | 当两个双字值相乘时,被乘数应位于EAX中,并且该乘数是存储 在存储器或另一个存储器中的双字值。生成的乘积存储在EDX:EAX 寄存器中,即,高32位存储在EDX寄存器中,低32位存储在EAX寄存器中 |
1 | MOV AL, 10 |
DIV/IDIV指令
除法运算生成两个元素-商和余数。在除法的情况下,可能会发生溢出。如果发生溢出,处理器将产生中断。DIV用于unsign数据,IDIV用于签名数据
1 | DIV/IDIV divisor |
情况 | 描述 |
---|---|
当除数是1个字节时 | 假定被除数在AX寄存器(16位)中。除法后,商进入AL寄存器,余数进入AH寄存器 |
当除数为1个字时 | 假定被除数在DX:AX寄存器32位长,高位16在DX中,低位16位在AX中。除后16位的商进入AX寄存器,其余16位进入DX寄存器 |
当除数为双字时 | 假定被除数在EDX:EAX寄存器中被除数64位长。高32位在EDX中,低阶32位位于EAX中。除后32位的商进入EAX寄存器,而32位的余数进入EDX寄存器 |
以下示例将8除以2.被除数8存储在16位AX寄存器中,除数2存储在8位BL寄存器中
1 | section .data |
逻辑指令
逻辑指令
处理器指令集提供指令AND、OR、XOR、TEST和NOT布尔逻辑
指令 | 格式 |
---|---|
AND | AND 操作数1,操作数2 |
OR | OR 操作数1,操作数2 |
XOR | XOR 操作数1,操作数2 |
TEST | TEST 操作数1,操作数2 |
NOT | NOT 操作数1 |
在所有情况下,第一个操作数都可以在寄存器或内存中。第二个操作数可以实在寄存器/内存,也可以是常数。但是,内存到内存操作是不可以的
AND指令
AND指令用于通过执行按位AND运算来支持逻辑表达式
1 | Operand1: 0101 |
AND操作可用于清除一个或多个位。例如,假设BL寄存器包含00111010,如果要将高阶位清零,则将其与0FH进行与运算
1 | AND BL,0FH |
如果要检查给定 的数字是偶数还是奇数,一个简单的测试是检查数字的最低有效位。如果为1 ,则为奇数,否则为偶数。假设数字在AL寄存器中,我们可以写
1 | AND AL,01H |
以下程序说明了这点
1 | section .data |
同样,要清楚整个寄存器,可以将其与00H进行与运算
OR指令
OR指令用于执行按位或运算来支持逻辑表达式。两个中的任意一个为一或都为一,则按位OR运算符将返回1。如果两个位均为零,则返回0
1 | Operand1: 0101 |
或运算可用于设置一个或多个位。如果AL寄存器包含0011 1010,则需要设置四个低阶位,可以将其与值0000 1111即0FH进行或运算
1 | OR bl,0fh |
1 | section .bss |
XOR指令
XOR指令实现按位XOR运算。当且仅当来自操作数的位不同时,XOR运算将结果置为1.如果来自操作数的位相同,则将结果位清零
1 | Operand1: 0101 |
将操作数与自身进行XOR会将操作数更改位0.这用于清楚寄存器
1 | XOR EAX,EAX |
TEST指令
TEST指令与AND运算的工作原理相同,但是与AND不同的时,他不会更改第一个操作数。因此,如果我们需要检查寄存器中的数字时偶数还是奇数,我们可以使用TEST指令执行此操作,无需更改原始数字
1 | TEST AL,01H |
NOT指令
NOT指令实现按位NOT运算。NOT操作将操作数中的位取反。操作数可以在寄存器中,也可以在存储器中
1 | Operand1: 0101 0011 |
条件
条件
汇编语言中的条件执行是通过几个循环和分支指令来完成的。这些指令可以更改程序的控制流。
- 无条件跳转 这是通过JMP指令执行的。条件执行通常涉及将控制权转移到不遵循当前执行指令的指令的地址。控制权的转移可以是前进的(执行新的指令集),也可以是后退的(重新执行相同的步骤)
- 无条件转移 这取决于条件由一组跳转指令j < condition >执行。条件指令通过中断顺序流程来转移控制,而他们通过更改IP中的偏移值来进行控制的
CMP指令
CMP指令比较两个操作数。它通常用于条件执行中。该指令基本上从一个操作数中减去一个操作数,以比较操作数是否相等。他不会干扰目标或源操作数。它与条件跳转指令一起用于决策
1 | CMP destination,source |
CMP 比较两个数字数据字段。目标操作数可以在寄存器中或在内存中。源操作数可以是常数数据,寄存器或内存
1 | cmp dx,00 |
CMP 通常用于比较计数器值是否已达到需要运行循环的次数。
1 | inc edx |
无条件跳转
如前所述,这是通过JMP指令执行的。条件执行通常涉及将控制权转移到不遵循当前执行指令的地址。控制权的转移是可以前进的(执行新的指令集),也可以是后退的(重新执行相同的步骤)
JMP提供一个标签名称,控制流将立即转移到改标签名称。JMP指令的语法是
1 | JMP label |
下面代码说明了JMP指令
1 | mov ax,00 |
条件跳转
如果在条件跳转中满足某些指定条件,则控制流程将转移到目标指令。
以下是用于算术运算的有符号数据的条件跳转指令:
指令 | 描述 | 标志测试 |
---|---|---|
JE/JZ | 跳转等于或跳转零 | ZF |
JNE/JNZ | 跳转不等于或跳转不为零 | ZF |
JG/JNLE | 跳转大于或跳转不小于/等于 | OF,SF,ZF |
JGE/JNL | 跳转大于/等于或不小于跳转 | OF,SF |
JL/JNGE | 跳转小于或不大于/等于 | OF,SF |
JLE/JNG | 跳转小于/等于或跳不大于 |
以下是对用于逻辑运算的无符号数据使用的条件跳转指令:
指令 | 描述 | 标志测试 |
---|---|---|
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/JNP | 跳校验或偶校验 | PF |
JNP/JPO | 跳转无就校验或跳转奇偶校验 | PF |
JS | 跳跃符号(负值) | SF |
JNS | 跳转无符号(正值) | SF |
循环
循环
JMP指令可以实现循环
1 | mov cl,10 |
但是处理器指令集包括一组用于实现迭代的循环指令。基本的loop指令具有以下语法
1 | LOOP label |
其中,label是标识目标指令的目标标签,如跳转指令中所述。LOOP指令假定ECX寄存器包含循环计数。当执行循环指令时,ECX寄存器递减,并且控制跳转至目标标签,直到ECX寄存器的值为零为止。
1 | mov ecx,10 |
例子
以下程序在屏幕上打印数字1到9
1 | section .text |
数字
数字
数值数据通常用二进制表示。算术指令对二进制数据进行操作,当数字显示在屏幕上或对键盘进行输入时,他们是ASCII形式。到目前为止,我们已经将该输入数据以ASCII形式转移为二进制以进行算术计算,并将结果转换回二进制。
1 | section .text |
但是此类转换会产生开销,并且汇编语言编程允许更有效的方式以二进制形式处理数字。小树可以以两种形式表示
ASCII形式
BCD或二进制编码的十进制形式
ASCII表示
在ASCII表示中,十进制数字存储为ASCII字符字符串。
例如,十进制值存储为1234
1 | 31 32 33 34h |
其中,32H是1的ASCII值,32H是2的ASCII值。有四个指令用于处理ASCII表示形式的数字
AAA-加法后ASCII调整
AAS-减法后ASCII调整
AAM-乘法后ASCII调整
AAD-除法前ASCII调整
这些指令不使用任何操作数,并假定所需的操作数位于AL寄存器中
1 | section .data |
BCD表示
BCD表示有两种类型
- 未打包的BCD表示
- 打包的BCD表示
在未压缩的BCD表示形式中,每个字节都存储一个十进制数字的二进制等效项
1 | 01 02 03 04h |
有两个指令来处理这些数字
- AAM-乘法后ASCII调整
- AAD-除法前后ASCII调整
四个ASCII调整指令AAA,AAS,AAM和AAD也可以与未打包的BCD表示一起使用。在打包的BCD表示中,每个数字使用四位存储。每个十进制数字打包成一个字节.例如,数字1234存储为
1 | 12 34h |
有两个指令来处理这些数字
- DAA-加法后的十进制调整
- DAS-减法后的十进制调整
打包的BCD表示形式不支持乘法和除法
以下程序将两个五位十进制数字加起来并显示总和
1 | section .text |
字符串
字符串
在前面的示例中,我们使用了 可变长度的字符串。可变长度字符串可以根据需要包含任意多个字符。通常我们通过以下两种方式之一指定字符串的长度
- 显示存储字符串长度
- 使用哨兵字符
我们可以使用表示位置计数器当前的$位置计数器符号来显示存储字符串长度
1 | msg db 'hello,world!',0xa |
$指向字符串变量msg的最后一个字符之后的字节。因此 $-msg给出字符串的长度
1 | msg db 'hello,world!',0xa |
另外,可以存储尾部定点字符的字符串来定界字符串,而不是显示存储字符串长度。前哨字符应为不出现在字符串中的特殊字符
1 | 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 | DS:SI | LODSB | LODSW | LODSD |
STOS | ES:DI,AX | STOSB | STOS | STOSD |
CMPS | DS:SI,ES:DI | CMPSB | CMPSW | CMPSD |
SCAS | ES:DI,AX | SCASB | SCASW | SCASD |
MOVS
MOVS指令用于将数据项(字节,字或双字)从源字符串复制到目标字符串。源字符串由DS:SI指向,目标字符串由ES:DI指向
1 | section .text |
LODS
在密码术中,凯撒密码是最简单的已知加密技术之一。在这种方法中,要加密的数据中的每个字母都被替换为字母下方固定数量位置的字母。在示例中,我们通过简单的将其中的每个字母替换为两个字母来加密数据,因此a将替换为c,b替换为d等。我们使用LODS将原始字符串‘password’加载到内存中
1 | section .text |
STOS
STOS指令将数据项从AL(对于字节-STOSB),AX(对于字-STOSW)或EAX(对于双字-STOSD)复制到目标字符串,该目标字符串由内存中的ES:DI指向。以下示例演示了如何是哟该LODS和STOS指令将大写字符串转换为小写值
1 | section .text |
CMPS
CMPS指令比较两个字符串。该指令比较DS:SI和ES:DI寄存器所指向的两个数据项,即一个字节,一个字或一个双字,并相应的设置标志。还可以将条件跳转指令一起使用。
1 | section .text |
SCAS
SCAS指令用于搜索字符串中的特定字符或一组字符。要搜索的数据项应该在AL(对于SCASB),AX(对于SCASW)或EAX(SCASD)寄存器中。要搜索的字符串应在内存中,并由ES:DI(或EDI)寄存器指向
1 | section .text |
重复前缀
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减为零时,它将停止。
数组
数组
汇编程序的数据定义指令用于为变量分配存储空间。变量也可以用一些特定的值初始化。初始化值可以是十六进制,十进制或二进制形式指定。
1 | MONTHS DW 12 |
数据定义指令也可以用于定义一维数组。
1 | NUMBERS DW 34,45,56,67,75,89 |
上面的定义声明了一个六个单词的数组,每个单词都用数字初始化。这个数组使用了2*6=12个字节的连续存储空间。第一个数字的字符地址为NUMBERS,第二个数字的符号地址为NUMBERS+2。
定义一个大小为8的双字节数组,并将所有值初始化为零
1 | INVENTORY DW 0,0,0,0,0,0,0,0 |
例子
以下示例通过定义一个3元素数组X来演示上述概念,该数组存储三个值:2、3、4
1 | section .text |
过程
过程
过程或子过程在汇编语言中非常重要,因为汇编语言程序文件往往会很大。程序由名称标识。在此名称之后,将描述执行明确定义的作业的过程主体。该过程的结束有return语句指示
1 | proc_name |
通过使用CALL指令从另一个函数调用该过程。CALL指令应将被调用过程的名称作为参数
1 | CALL proc_name |
被调用过程通过使用RET指令将控制权返回给调用过程
1 | sum: |
堆栈数据结构
堆栈是内存中类似数组的数据结构,可以在其中存储数据并从称为”堆栈顶部“的位置删除数据。需要存储的数据被“推送”到堆栈中,要检索的数据从堆栈中“弹出”。堆栈是一种LIFO数据结构,即首先存储的数据最后被检索。汇编语言为堆栈操作提供了两条指令:PUSH和POP
1 | PUSH operand |
堆栈段中保留的内存空间用于实现堆栈。寄存器SS和ESP(或SP)用于实现堆栈。SS:ESP寄存器指向堆栈的顶部,该顶部指向插入到堆栈中的最后一个数据项,其中SS寄存器指向堆栈段的开头,而SP(或ESP)将偏移量设置为堆栈段。
堆栈实现具有以下特征:
- 只能将字或双字存储到堆栈中,而不是字节
- 堆栈朝反方向增长,即朝着较低的存储器地址增长
- 堆栈的顶部指向插入堆栈中的最后一个项目。他指向插入的最后一个字的低字节
1 | PUSH AX |
以下程序显示整个ASCII字符集。主程序调用一个名为display的过程,该过程显示ASCII字符集
1 | section .text |
递归
递归
递归过程是一个调用自身的过程。递归有两种:直接和间接。在直接递归中,过程调用自身,在间接递归中,第一个过程调用第二个过程,第二个过程依次调用第一个过程。
1 | section .text |
宏
宏
编写宏是确保使用汇编语言进行模块化编程的另一种方法
- 宏是由名称分配的指令序列,可以在程序中的任何位置使用
- 在NASM中,宏使用%macro和%endmacro指令定义
- 宏以%macro指令开头,以%endmacro指令结尾
宏定义的语法
1 | %macro macro_name number_of_params |
其中,number_of_params指定数字参数,macro_name指定宏的名称。
通过使用宏名称和必要的参数来调用宏。当需要在程序中多次使用某些指令序列时,可以将这些指令放在宏中并使用它,不必一直写指令。
1 | %macro write_string 2 |
文件管理
文件管理
系统将任何输入或输出数据视为字节流。有三个标准文件流
- 标准输入(stdin)
- 标准输出(stdout)
- 标准错误(stderr)
文件描述符
文件描述符是分配给一个文件作为文件ID的16位整数。创新新文件或打开现有文件时,文件描述符用于访问文件。标准文件流的文件描述符-stdin,stdout和stderr分别位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_clase | unsigned int | - | - |
8 | sys_create | const char * | int | - |
19 | sys_lseek | unsigned int | off_t | unsigned int |
使用系统调用所需的步骤与我们之前讨论的相同
- 将系统调用号码放入EAX寄存器中
- 将参数保存到系统调用中的寄存器EBX,ECX等中
- 调用相关中断(80h)
- 结果通常在EAX寄存器中返回
创建和打开文件
要创建和打开文件,请执行以下人物
- 将系统调用sys_create()数字8放入EAX寄存器
- 将文件名放入EBX寄存器中
- 将文件权限放入ECX寄存器
系统调用在EAX寄存器中放回以创建的文件描述符,如果发生错误,则错误代码在EAX寄存器中
打开一个现有文件
要打开现有文件
- 将系统调用sys_open() 5 放入EAX寄存器中
- 将文件名放在EBX寄存器中
- 将文件访问模式放入ECX寄存器中
- 将文件权限放入EDX寄存器中
- 系统调用在EAX寄存器中返回已创建文件的文件描述符,如果发生错误,则错误代码在EAX寄存器中。
从文件读取
要从文件读取
- 将系统调用sys_read() 3 放入EAX寄存器中
- 将文件描述符放入EBX寄存器
- 将指针放到ECX寄存器中的输入缓冲区
- 将缓冲区大小(即要写入的字节数)放入EDX寄存器中
- 系统调用返回写入EAX寄存器的实际字节,如果发生错误,则错误代码位于EAX寄存器中
关闭文件
- 将系统调用sys_close() 编号6 放入EAX寄存器中。
- 将文件描述符放入EBX寄存器。
- 发生错误时,系统调用将返回EAX寄存器中的错误代码。
更新文件
- 将系统调用sys_lseek() 编号19 放入EAX寄存器中。
- 将文件描述符放入EBX寄存器。
- 将偏移值放入ECX寄存器中。
- 将偏移量的参考位置放入EDX寄存器中。
参考位置可以是:
文件开头-值0
当前位置-值1
文件结尾-值2
发生错误时,系统调用将返回EAX寄存器中的错误代码。
内存管理
内存管理
sys_brk()系统调用是由内核提供的,而不需要后移动他的分配内存。调用在内存中的引用程序映像后面分配内存。此系统功能可以在数据部分中设置最高可用地址。参数为最高内存地址,存储在EBX寄存器中。发生任何错误,sys_brk()返回-1或返回负错误代码本身。
以下程序使用sys_brk()系统调用分配16kb的内存
1 | section .text |