汇编语法

汇编是种低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。普遍地说,特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。

两种汇编风格差异对比

X86汇编语言风格有两种汇编风格:AT&T 和 Intel 风格。

寄存器命名

  • AT&T风格中,寄存器会加上%作为前缀

  • Intel汇编中寄存器名是不需要加前缀的.可以直接使用.

AT&T风格 Intel风格 说明
push %eax push eax 这是一条入栈指令,把寄存器eax中的值压入栈中

立即数格式

  • 在AT&T 汇编中 , 用$前缀表示一个立即数.

  • 在Intel 汇编中 , 立即数没有任何前缀. 直接用一个数字表示. (当然有不同的进制. 比如 0x01 , 10 等)

AT&T风格 Intel风格 说明
push $1 push 1 把一个立即数压入栈中

操作数顺序

AT&T和Intel格式中的源操作数和目标操作数的位置正好相反,下面是给寄存器EAX赋一个初值1

  • AT&T风格: 操作符 源操作数 , 目的操作数 mov $1 , %eax

  • Intel 风格:操作符 目的操作数 , 源操作数 mov eax , 1

内存操作数的寻址方式

  • AT&T寻址格式: section:disp(base, index, scale)

  • Intel 寻址格式: section:[base + index*scale + disp]

无论形式如何,都是实现如下的地址计算:

disp + base + index * scale
# 最终地址 = 地址或偏移 + %基址或偏移量寄存器 + %索引寄存器 * 比例因子

其中base和index必须是寄存器,disp和scale可以是常数

AT&T格式 Intel格式
movl -4(%ebp), %eax mov eax, [ebp - 4]
movl array(, %eax, 4), %eax mov eax, [eax*4 + array]
movw array(%ebx, %eax, 4), %cx mov cx, [ebx + 4*eax + array]
movb $4, %fs:(%eax) mov fs:eax, 4

数据宽度表示

  • 在AT&T汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀b、w、l分别表示操作数为字节(byte,8比特)、字(word,16比特)和长字(long,32比特)

  • 在Intel汇编格式中,操作数的字长是用byte ptr和word ptr等前缀来表示的

另附:objdump反汇编

  • objdump -d <file(s)>: 将代码段反汇编;

  • objdump -S <file(s)>: 将代码段反汇编的同时,将反汇编代码与源代码交替显示,编译时需要使用-g参数,即需要调试信息;

  • objdump -C <file(s)>: 将C++符号名逆向解析

  • objdump -l <file(s)>: 反汇编代码中插入文件名和行号

  • objdump -j section <file(s)>: 仅反汇编指定的section

常用反汇编命令说明:

  • 在linux上使用 objdump -d <file> 反汇编生成 AT&T 格式的汇编代码

  • 在linux上使用 objdump -d -mi386:x86-64:intel 反汇编生成 Intel 格式的汇编代码

汇编语句语法

每个语句都遵循以下格式:

[标签] 指令 [操作数] [; 注释]

其中方括号中字段是可选的。

汇编程序分段

汇编程序一般分为 3 个段

  • data 段

  • bss 段

  • text 段

data 段

data(数据)段被用于声明初始化的数据或常数。此数据在运行时候不会更改。您可以在段中声明各种常量值,文件名或缓存区大小等

声明数据段的语法是:

.section.data

bss 段

BSS(Block Started by Symbol)是程序中未初始化的全局变量和静态变量所在的一块内存区域。在程序运行时候,BSS段中的变量会被自动初始化为0或空值。

BSS段通常位于数语句段的末尾,并且与数据段在内存中相邻。程序中所有未被初始化的全局变量和静态变量都会被设置在 BSS 段中。

BSS段的大小取决于程序中为初始化的全局变量和静态变量的数量和大小。程序可以使用 size 命令或其它工具查看 BSS 段大小。

声明bss段的语法是:

.section.bss

text 段

text段用于保存实际的代码,该段必须以全局声明 _start开头,该声明告诉内核程序从何处开始执行

声明代码段的语法是:

.section.text
    global _start

_start:

汇编程序内存分段

分段存储模型将系统存储器分为独立的分段组,这些分段有位于分段寄存器中的指针引用。每个分段用于包含特定类型的数据。

  • 代码段:用于存放程序指令。在内存布局图中,代码段通常位于程序的起始位置,并且向高地址方向增长。程序运行时候,CPU会从代码段中读取指令并执行

  • 数据段:用于存放程序中全局变量和静态变量。在内存布局图中,数据段通常位于代码段之后,并且向高地址方向增长。程序运行时候,全局变量和静态变量会被存储到数据段中

  • 堆栈段:用于存放程序的堆栈。在内存布局图中,堆栈段通常位于数据段之后,并且向低地址方向增长。程序运行时候,堆栈段中的空间用于存储函数的参数、局部变量和返回地址等信息

  • BSS段:用于存放为初始化的全局变量和静态变量。在内存布局图中,BSS段通常位于数据段之后,并且向高地址方向增长。程序运行时,BSS段中的变量会被自动初始化为0或空值。

+----------------+
|    代码段      |
+----------------+
|    数据段      |
+----------------+
|    堆栈段      |
+----------------+
|    BSS段       |
+----------------+

汇编语言寄存器

IA-32体系结构中有 10 个32为和 6 个 16 位处理器寄存器。

存储器分为三类:

  • 通用寄存器

  • 控制寄存器

  • 段寄存器

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

  • 数据寄存器

  • 指针寄存器

  • 索引寄存器

  • 数据寄存器

通用寄存器

数据寄存器

4个32位数据寄存器用于算术、逻辑和其它操作。这些32位寄存器可以用3中方式使用:

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

  • 32位寄存器的下半部分可以用作4个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寄存器存储下一条要执行的指令的内存地址。在程序执行过程中,CPU需要不断从内存中读取指令并执行,IP寄存器记录了下一条要执行的指令的内存地址,CPU会根据这个地址从内存中读取并执行它。当指令执行完毕后,IP寄存器会被更新位下一条指令的地址,如此循环。在函数调用时候,IP寄存器也被用来存储返回地址。当函数被调用时候,CPU会将当前指令的地址(即IP寄存器中的值)轧入栈中,然后跳转到函数的入口地址。当函数执行完毕之后,CPU会从栈中弹出返回地址,并把它存储到IP寄存器中,以便程序继续执行调用函数之后的指令。

  • 堆栈指针(SP):16位SP寄存器用于存储栈顶指针,指向当前栈帧的栈顶。栈是一种后进先出的数据结构,在程序执行过程中经常被用于存储函数调用的上下文、局部变量和临时数据等。SP寄存器记录了当前栈帧的栈顶指针,用于栈的push和pop操作。当一个数据被push到栈中时候,SP寄存器的值会减少一个固定的偏移量,指向新的栈顶位置;当一个数据从栈中pop出来时候SP寄存器的值会增加一个固定的偏移量,指向新的栈顶位置。在函数调用时候,SP寄存器也被用于存储当前栈帧的基址指针(BP寄存器),以便在函数返回时恢复调用函数的上下文。当一个函数被调用时候,SP寄存器的值会减少一个固定的偏移量位函数调用分配栈空间;当函数返回时候,SP寄存器的值会增加一个固定的偏移量,释放栈空间。

  • 基本指针(BP):16位BP寄存器用于存储栈帧中的基址地址,通常在处理函数调用和返回时候使用。BP寄存器的作用是用于寻址当前栈帧中的局部变量和参数。当一个函数被调用时候,BP寄存器会保存当前栈帧的基址,指向栈的底部,也就是存储函数参数和局部变量的区域。在函数执行过程中,BP寄存器可以通过偏移量访问函数参数和局部变量。在函数调用时候,BP寄存器会被保存到栈中,以便在函数返回时候恢复调用函数的上下文。当函数返回时候,BP寄存器的值会被弹出栈中,恢复到调用函数的上下文中,以便程序继续执行。

索引寄存器

索引寄存器是 32 位 ESI、EDI寄存器,以及相应的 16 位 SI、DI寄存器,用于索引寻址。

索引寄存器可以用于存储数组元素的偏移量或指针,以便程序可以通过偏移量或指针访问数组中的元素。例如:如果有一个数组 a,可以使用SI寄存器存储数组元素的偏移量,然后通过指令MOV AX,[a+SI]来读取数组元素的值。

索引寄存器还可以用于存储字符串的地址,以便程序可以对字符串进行操作。例如,可以使用SI寄存器存储字符串的地址,然后通过指令MOV AX,[SI]来读取字符串中的一个字符。

在某些情况下,程序还可以同时使用基址寄存器和索引寄存器来进行地址计算。例如:可以使用BX寄存器存储数组的基址,使用SI寄存器存储数组元素的偏移量,以便程序可以通过指令MOV AX.[BX+SI] 来读取数组元素的值。

  • 源索引(SI):用于存储字符串的源索引

  • 目标索引(DI):用于存储字符串的目标索引

控制寄存器

将 32 位的指令指针寄存器和 32 位的标志寄存器组合起来视为控制寄存器。许多指令设计比较、数学运算、更改标志状态,而其它一些条件指令则测试这些状态标志的值,以控制将流程带到其它位置。

通用标志位是:

  • 溢出标志(OF):有符号算术运算后数据的高阶位移出(最左边位)。

  • 方向标志(DF):它确定向左或向右移动或比较字符串数据的方向。DF值位0时,字符串操作为从左到右的方向;当DF值为1时,字符串操作为从右向左的方向

  • 中断标志(IF):确定是否忽略或处理外部中断(例如键盘输入等)。当值为0时候,它禁用外部中断,当值位1时,它使能中断。

  • 陷阱标志(TF):允许在单步模式下设置处理器的操作。我们使用的 DEBUG 程序设置了陷阱标志,因此我们可以一次逐步执行一条指令

  • 符号标志(SF):显示算术运算结果的符号。根据算术运算后数据项的符号来设置此标志。该符号由最左边的高位指示。正结果将SF的值清除为0,负结果将其设置为1.

  • 零标志(ZF):存储者上次算术或比较运算结果是否为0。如果运算结果为 0,ZF 标志位被设置为 1,否则被设置为0.

  • 辅助进位标志(AF):存储上次运算是否发生了半进位。当两个低位相加后产生进位时,AF 标志为被设置为1,否则为0。补充:半进位,表示算术运算BCD码第 3 位到第 4 位的进位。

  • 奇偶校验标志(PF):表示上次运算结果中1的个数是否为偶数。当结果中 1 的个数为偶数时候,PF 标志位被设置为 1,否则被设置为 0。

  • 进位标志(CF):表示上次运算是否产生了进位。当一个无符号数加法运算或逻辑右移运算产生进位时候,CF标志位被设置为1,否则为0。

段寄存器

段是程序中定义的特定区域,用于包含数据、代码、堆栈。有 3 个主要部分:

  • 代码段(CS):代码段是程序中存放指令的一块内存区域,通常位于程序的数据段上方。在程序执行时候,CPU需要不断从代码段中取出指令并执行,程序可以使用CS寄存器来指定代码段的地址,以便CPU可以正确的访问代码段中的指令。

  • 堆栈段(SS):存储着堆栈段的地址。堆栈段是程序中用于存储函数调用栈和局部变量的一块内存区域,通常位于程序的数据段下方。当程序需要执行函数调用或局部变量存取操作时候,需要使用堆栈段来存储相应的信息。程序可以使用SS寄存器来指定堆栈段的地址,以便CPU可以正确的访问堆栈段中的数据。在X86架构中,堆栈是从高地址向低地址生长的,程序可以使用 SP 寄存器来指定堆栈指针的位置。

  • 数据段(DS):数据段是程序中存放全局变量和静态变量的一块内存区域,通常位于程序的堆栈段下方。当程序需要访问全局变量或静态变量时候,需要使用数据段来存储相应的数据。程序可以使用DS寄存器来指定数据段的地址,以便CPU可以正确的访问数据段中的数据。

  • 附加数据段(ES):附加数据段是程序中用于临时存放数据的一块内存区域,通常用于存储一些与程序数据分离但又需要频繁访问的数据,例如:字符串、数组等。在 X86 架构中 ES寄存器通常与 DI 寄存器 或 SI 寄存器一起使用,用于访问字符串或数组等数据结构。

汇编语言定义变量

定义变量本质是预留一定大小的空间留待程序执行过程中使用。

为初始化数据分配空间

汇编程序中,可以使用 DB、DW、DD、DW等指令来定义变量

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

语法:

[变量名] 指令 初始值 [,初始值2]...

例子:

a DB 'y'
b DW 12345
c DW -12345
d DQ 123
e DD 1.234
f DQ 123.456

注意:

  1. 字符的每个字节均以十六进制形式存储为其 ASCII 值。

  2. 每个十进制值都将自动转换为其等效的 16 位二进制数,并以十六进制数形式存储。

  3. 处理器使用小尾数字节顺序。

  4. 负数将转换为其 2 的补码表示形式。

  5. 短浮点数和长浮点数分别使用 32 位或 64 位表示。

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

汇编语言中,可以使用 RESB、RESW、RESD、RESQ 为为初始化变量分配空间。这些指令用于在程序的BSS段中为变量分配指定大小的内存空间,但不会为变量初始化任何值。

指令 作用 存储空间
RESB 分配 1 个字节类型的内存空间 1 字节
RESW 分配 1 个字类型的存储空间 2 字节
RESD 分配 1 个双子类型的存储空间 4 字节
RESQ 分配 1 个四字类型的存储空间 8 字节

语法:

变量名 指令 数量

例子:

section .bss

a RESB 1
b RESD 1

汇编常量

常量定义使用 EQU 指令

语法:

常量名 EQU 常量值

例子:

section .data

a EQU 10

汇编数字

数值数据通常用二进制表示。算术指令对二进制数据进行操作。当数字显示在屏幕上或从键盘输入时,它们是 ASCII 形式。到目前为止,我们已经将该输入数据以 ASCII 形式转换为二进制以进行算术计算,并将结果转换回二进制。

然而,这种转换有性能损耗,汇编语言支持更高效的方式处理二进制形式的数字。

十进制数字可以用两种形式:

  • ASCII 格式

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

ASCII 表示

在 ASCII 表示中,十进制数字存储为 ASCII 字符字符串。

例如,十进制值 1234 存储为:

31323334H

其中:31H 是 1 的 ASCII 值;32H 是 2 的 ASCII 值,以此类推。

有 4 种指令用于处理 ASCII 表示形式的数字:

  • AAA:加法后 ASCII 调整

  • AAS:减法后 ASCII 调整

  • AAM:乘法后 ASCII 调整

  • AAD:除法后 ASCII 调整

这些指令不使用任何操作数,并假定所需的操作数位于 AL 寄存器中。

BCD 表示

BCD 表示有两种类型:

  • 未打包的 BCD 表示

  • 打包的 BCD 表示

在未打包的 BCD 表示形式中,每个字节都存储一个十进制的二进制等效项。例如:数字 1234 存储为:

01  02  03  04H

有两种指令来处理这些数字:

  • AAM:乘法后 ASCII 调整

  • AAD:除法后 ASCII 调整

四个 ASCII 调整指令 AAA,AAS,AAM 和 AAD 也可以与未打包的 BCD 表示一起使用。在打包的 BCD 表示中,每个数字使用四位存储。两个十进制数字打包成一个字节。

例如,数字 1234 存储为:

1234H

有两个指令来处理这些数字:

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

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

打包的 BCD 表示形式不支持乘法和除法

汇编语言字符串

通常我们通过以下两种方式之一来指定字符串的长度:

  • 显示存储字符串长度

  • 使用哨兵字符

字符串指令

每个字符串指令可能需要一个源操作数,一个目标操作数或两者。对于 32 位段,字符串指令使用 ESI 和 EDI 寄存器分别指向源和目标操作数。但是,对于16位段,SI和DI寄存器分别用于指向源和目标。

有 5 种用于处理字符串的基本说明,他们是:

  • 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 和 ES:DI 寄存器分别指向源和目标操作数。假定源操作数位于内存中的 DS:SI,目标操作数位于 ES:DI。

下表提供了各个版本的字符串指令和假定的操作数空间: |基础指令|操作的寄存器|字节运算|字运算|双字运算| |—-|—-|—-|—-|—-| |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 是否为0.重复指令处理,直到 CX 为 0 为止。

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

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

  • 使用 STD (设置方向表示,DF=1)使操作从右到左

REP 前缀也有以下变化:

  • REP:是无条件的重复。重复该操作,直到 CX 为零为止。

  • REPE或REPZ:这是有条件的重复。当零标志等于零时,它将重复操作。当ZF表示不等于0或CX为0时,它将停止

  • REPNE或REPNZ:这也是有条件的重复。当零标志表示不等于0时,它将重复操作,当ZF指示等于0或CX减为0时,它将停止。

rep movsb   ; 将一个字符串从一个内存位置复制到另一个内存位置。
            ;   rep 指令将 AL 寄存器中的值作为 循环计数器,
            ;   并将 DS:SI 指针指向源字符串起始位置,
            ;   将 ES:DI 指针指向目标字符串的起始位置。
            ;   重复执行直到 CX 为0

汇编数组

数组定义

与定义变量一样,使用DBDWDDDQ指令可以定义数组:

arr1  DB  10, 20, 30, 40
arr2  DW  100, 200, 300, 400

TIMES伪指令

TIMES伪指令用于重复执行某个操作或指定一定次数

TIMES指令的一般形式如下:

TIMES 次数  要执行的指令或操作

TIMES 指令通常用于以下几种情况:

  • 重复定义多个数据项。例如:代码使用 TIMES 指令定义了 10 个字节,每个字节初始值为0:arr1 TIMES 10 DB 0

  • 重复执行一条指令或操作:TIMES 10 NOP

  • 重复执行一段代码段:

TIMES 10
    MOV eax, [ebx]
    add eax, ecx
    mov [ebx], eax
    loop .loop

需要注意的是:TIMES指令中的count必须是一个整数常量。

堆栈数据结构

堆栈是内存中类似数组的数据结构,可以在其中存储数据并从称为堆栈 “顶部” 的位置删除数据。需要存储的数据被 “推” 到堆栈中,要检索的数据被从堆栈中 “弹出” 出来。堆栈是后进先出的数据结构,即先存储的数据最后检索。

汇编语言为堆栈操作提供了两条指令:PUSHPOP。这些指令语法如下:

PUSH  操作数
POP   地址/寄存器

堆栈段中保留的内存空间用于实现堆栈。

寄存器 SS 和 ESP(或 SP)用于实现堆栈。

SS:ESP 寄存器指向堆栈的顶部,该顶部指向插入到堆栈中的最后一个数据项,其中 SS 寄存器指向堆栈段的开头,而 SP(或 ESP)将偏移量设置为堆栈段。

堆栈实现具有以下特征:

  • 只能将字或双字保存到堆栈中,而不是字节。

  • 堆栈朝反方向增长,即朝着较低的存储器地址增长

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

算术指令

INC 指令

INC 指令用于将操作数加 1

语法:

INC DL      ; 寄存器中值 +1
INC [count] ; 变量 count +1

DEC 指令

DEC 指令用于将操作数减 1

语法:

DEC DL
DEC [count]

ADD指令 和 SUB指令

ADDSUB指令用于对字节、字、双字大小的二进制数据进行简单的加/减。

语法:

ADD/SUB 目标, 源

MUL指令 和 IMUL指令

二进制数据相乘有两条指令。MUL(乘法)指令处理无符号数据,IMUL(整数乘法)处理有符号数据。这两条指令都会影响进位和移出标志。

以下部分说明了三种不同情况下的 MUL 指令:

  1. 当两个字节相乘时候:AL x 8bit source = AH AL

  2. 当两个单字值相乘时候:AX x 16bit source = DX AX

  3. 当两个双字值相乘时候:EAX x 32bit source = EDX EAX

DIV指令 和 IDIV指令

除法运算生成两个元素————商和余数。

在乘法的情况下,不会发生溢出,因为使用双长度寄存器来保存乘积。然而,在除法的情况下可能会发生溢出,如果发生溢出,处理器将生成终端。DIV指令用于无符号数据,IDIV用于有符号数据。

语法:

DIV/IDIV  除数  ; 被除数放在累加器中

以下说明3种操作数大小不同的除法情况:

  1. 当除数为1字节时候: AX(16 bit) / (8 bit) = AL(商) + AH(余数)

  2. 当除数为1单字时候: DX AX(32 bit) / (16 bit) = AX(商) + DX(余数)

  3. 当除数是双字时候:EDX EAX(64 bit) / (32 bit) = EAX(商) + EDX(余数)

逻辑指令

指令 格式
AND AND 操作数1, 操作数2
OR OR 操作数1, 操作数2
XOR XOR 操作数1, 操作数2
TEST TEST操作数1, 操作数2
NOT NOT 操作数1

结果设置 CF、OF、PF、SF、DF

条件跳转指令

  • 无条件跳转:这由 JMP 指令执行条件执行通常涉及将控制权转移到当前执行指令后面的指令的地址控制权的转移可以向前,以执行一组新的指令,也可以向后,以重新执行相同的步骤。

  • 条换跳转:这由一组跳转指令 j 执行,具体取决于条件条件指令通过中断顺序流来传输控制,并通过更改 IP 中的偏移值来实现

无条件跳转

  1. JMP:提供了一个标签名称,语法:JMP 标签

条件跳转

算术运算的有符号数据条件跳转指令

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

逻辑运算的无符号数据使用的条件跳转指令

指令 描述 标志测试
JE/JZ 等于 或 等于0 跳转 ZF
JNE/JNZ 不等于 或 不为0 跳转 ZF
JA/JNBE 向上 或 不低于/等于 跳转 CF, ZF
JAE/JNB 大于/等于 或 不小于 跳转 CF
JB/JNAE 低于 或 不大于/等于 跳转 CF
JBE/JNA 低于/等于 或 不大于 跳转 AF, CF

循环指令

JMP指令实现循环。例如,以下代码段可实现循环 10 次:

MOV CL, 10

L1:
 ; <循环体>
DEC CL
JNZ L1

使用loop指令

loop 标签

循环指令假定 ECX 寄存器包含循环计数。当执行循环指令时,ECX 寄存器递减,并且控制跳至目标标签,直到 ECX 寄存器的值(即计数器达到零)为止。

上边代码等价于:

mov ECX, 10
l1:
<循环体>

loop l1

汇编寻址模式

寻址的三种基本模式:

  • 立即寻址

  • 寄存器寻址

  • 直接寻址

  • 寄存器间接寻址

  • 寄存器相对寻址

  • 基址变址寻址

  • 比例变址寻址

立即寻址

将一个常数或者表达式作为操作数,直接存储在指令中

例子:

MOV  AX, 1234H

寄存器寻址

将一个寄存器中的值作为操作数

例如:

MOV  DX,  TAX_RATE  ; 寄存器是第一个操作数
MOV  COUNT, CX      ; 寄存器是第二个操作数
MOV  EAX, EBX       ; 两个操作数都是寄存器
MOV  AX, BX

直接寻址

将一个内存单元的地址直接作为操作数

例如:

MOV  AX, [1234H]

寄存器间接寻址

使用寄存器中存储的地址来访问内存

例如:

MOV  AX, [BX]

寄存器相对寻址

使用一个固定的偏移量加上一个寄存器中存储的地址来寻址

例子:

MOV  AX, [BX+10H]

基址变址寻址

使用两个寄存器,其中一个存储基地址,另一个存储偏移量

例子:

MOV  AX, [BX+SI]

比例变址寻址

使用两个寄存器,其中一个存储基地址,另一个存储偏移量,并且偏移量还要乘以一个固定的比例因子

例如:

MOV  AX, [BX+SI*2]

汇编语言过程

过程也称为子程序

语法:

过程名字:
    过程体
    ....
    ret

通过使用 CALL 指令从另一个函数调用该过程,语法如下:

CALL 过程名字

被调用的过程使用 RET 指令将过程返回给调用过程。

汇编宏

Intel 宏写法

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

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

  • 在 NASM 中,宏是用 %macro%endmacro 指令定义的

  • 宏以 %macro 指令开始,以 %endmacro 指令结束

AT&T 宏写法

  • 以 .macro 开始,以 .endm 宏结束

例子:

.macro loop count
    mov $0, %eax
    1: 
        cmp $count, %eax
        jge 2f
        // 循环体代码
        inc %eax
        jmp 1b
    2:
.endm

其它指令补充

lidt

lidt 是一个汇编指令,用于将中断描述符表(I年头儿如平台D二手车日普通人T阿勃勒, IDT)的地址加载到 IDTR 寄存器中。IDT 是用于中断处理的重要数据结构,它包含一组描述中断处理程序位置和特权级别的描述符。

lidt 指令语法:

lidt [idtr]

该指令将指定内存地址中的 6 字节内容读入 IDTR 寄存器。idtr参数是一个内存地址或内存地址表达式,指向一个包含IDT信息的数据结构。

IDTR寄存器是一个 48 位寄存器:

  • 它的低 16 位存储 IDT 的大小(以字节为单位)

  • 它的高 32 为存储 IDT 的基地址

注意:lidt 指令只能在特权级别为 0 的代码段中执行,因为只有内核代码才有权访问 IDT。在用户模式下访问 IDTR 会触发 “general protection fault” (一般保护故障)。

lgdt

lgdt 是一个汇编指令,用于将全局描述符表(Global Descriptor Table, GDT)的地址加载到 GDTR 寄存器中。GDT是一个包含所有段描述符的表,用于管理内存分段和保护。

lgdt 指令语法:

lgdt [gdtr]

该指令将指定的内存地址中的 6 字节内容读入 GDTR 寄存器。gdtr 参数是一个内存地址或内存地址表达式,指向一个包含 GDT 信息的数据结构。

GDTR 寄存器是一个 48 位寄存器

  • 它的低 16 位存储 GDT 的大小(以字节为单位)

  • 高 32 位存储 GDT 的基地址。

在执行 lgdt 指令后,CPU 将使用 GDTR 寄存器中的值来定位 GDT。

例子:

gdt_desc: 
    dw gdt_end - gdt - 1    ; gdt size
    dd gdt                  ; gdt address

lgdt [gdt_desc]

在这个例子中,gdt是一个 GDT 数组

  • gdt_end 是 GDT 数组的末尾地址。

  • gdt_desc 是一个包含 GDT 信息的数据结构,它包括一个 16 位的 GDT 大小和一个 32 位的 GDT 基址。

lgdt指令将 gdt_desc 中的值加载到 GDTR 寄存器中,从而告诉 CPU 在哪里找到 GDT。

注意:lgdt指令只有在特权级别为 0 的代码段中执行,因为只有内核代码才有权利访问 GDT。在用户模式下,访问 GDTR 寄存器会触发”general protection fault”(一般保护故障)。

伪指令 .org

.org用于指示汇编器在生成机器码时候从指定的地址开始。这个指令通常用于在汇编代码中定义数据区或代码区的起始位置

语法:

.org <地址>

.org 之前的汇编代码将从地址 0 开始生成机器码,直到遇到 .org 指令。

例子:

.org 0x1000                     ; 从地址 0x1000 开始生成机器码
data_section:
    db 0x11, 0x22, 0x33, 0x44   ; 定义一个 4 字节的数据

注意:.org 可能不受所有汇编器支持,但是一定有其它指令可以实现相同效果

lmsw

用于将指定控制寄存器中的低 16 位(也称为“状态字”)加载到指定的内存地址中

语法:

lmsw  [mem]

该指令将 CR0 控制寄存器的低 16 位(即状态字)加载到指定的内存地址中。mem 参数是一个内存地址和内存地址表达式,指向要存储状态字的位置。

例子:

data_section:
    db  0x00            ; 存储状态字的位置

.code
    mov eax, cr0        ;  CR0 控制寄存器的值加载到 EAX 
    lmsw [data_section] ; 将状态字存储到指定的内存地址中

例子中,data_section 是一个用于存储状态字的位置。在代码中,mov 指令将 CR0 控制寄存器的值加载到 EAX 中,然后 lmsw 指令将状态字从EAX中取出并存储到 data_section 指定的内存地址中。

需要注意的是,lmsw 指令只能在特权级别位 0 的代码中执行,因为只有内核代码才有权修改控制寄存器。在用户模式下,执行 lmsw 指令会触发 “general protection fault” (一般保护故障)。 此外,lmsw 指令只能修改 CRO 控制寄存器的低 16 位,不能修改其它控制寄存器。

jmpi

它是一条跳转指令,将程序的控制权转移到地址 0x8 处。它是 IA-64 指令集中的一条指令,目前IA-64指令已经过时

语法:

jmpi  <target>, <rot>
  • target 参数:是跳转目标地址的表示形式,可以是一个直接的地址、一个标签(代表一个地址)或者一个寄存器(代表一个地址)

  • rot 参数:是一个旋转位数,用于指定跳转目标地址的位移量。在执行 jmpi 指令后,CPU将计算出跳转目标地址,并继续执行指令流中位于该地址处的代码。

例子:

jmpi 0,8

将程序控制权转移到 0x8 处。

需要注意的是:由于 jmpi 指令使用的是旋转地址,因此在实际计算跳转目标地址时候,地址 0x8 实际上会被旋转 8 位,变成:0x80000000000000080。因此 jmpi 0,8 实际上是将程序控制权转移到地址 0x8000000000000080 处。

注:地址旋转是 IA-64 指令集中的一种特殊的地址计算方式,它通过将地址的高位和低位进行旋转,从而实现对地址的偏移。在 IA-64 中,地址旋转的位数以旋转数来表示,它是一个 6 位的立即数,可以在指令中直接指定。 例如:jmpi 0,8 这条指令中的旋转数为 8,表示将地址的高 8 位和低 56 位进行旋转,从而实现对跳转地址的偏移。具体来说,指令中的 0 表示跳转地址的低 56 位(64位地址中的后7个字节),而8表示跳转地址的高8位(64位地址中的前一个字节)。因此,在执行 jmpi 0,8时候,CPU会将跳转地址的高 8 位 和 低 58位进行旋转,得到实际的跳转地址 0x80000000000000080

注意:地址旋转智能用于 IA-64 指令集中的指令,并不是用于其它指令集中的指令。

旋转过程

IA-64指令中地址位数 64,地址旋转位数是一个 6 位的立即数,旋转过程如下:

  • 将 64 位地址按字节分割成 8 个 8 位字节

  • 将地址的 高8位 和 低56位 进行旋转,旋转数由指令中的旋转数指定。旋转数取值范围是 0-63 之间的任意整数,其中 0 表示不旋转,63 表示完全旋转。

  • 将旋转后的 高8位 和 低56位 再次合并

例子:

0x12 0x34 0x56 0x78 0x9a 0xbc 0xde 0xf0     ; 8x8 64 位,1. 分成 8 个 8 字节

; 旋转数位n,表示将地址高 n 位旋转到低56位, 将低56-n位旋转到 高8位。
; 如果旋转数为0表示地址不发生旋转

; 如果旋转数为 8 则上面地址将进行如下旋转:

0xde 0xf0 0x12 0x34 0x56 0x78 0x9a 0xbc     ; 这个旋转过程将原来地址高8位(0x12)旋转到低56位,将原来地址低 56-8=48 位旋转到了高 8 位。

; 旋转后高 8 位 和低56位组合成新的 64 位地址为:

0xde 0xf0 0x12 0x34 0x56 0x78 0x9a 0xbc

旋转 jmpi 0,8 旋转过程:

; 1. 将目标地址拆分成 8 个 8 字节:
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

; 2. 将旋转数 8 转换为二进制数 000100 (注意高低位,这个值右边才是高位,汇编语言开始到结尾 是 低位到高位的过程),将它作为旋转字段填入指令中
0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x00

; 3. 将地址的高8位和低56位进行旋转,旋转数为8,因此需要将地址高 8 位旋转到低 56 位,将地址的低 48 位旋转到高 8 位,旋转后的结果如下
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x80

; 4. 旋转后的高 8 位和低56位重新组合成新的 64 位地址 2 和 3 组合,得到最终结果:
0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x80

; 注意第4步中左边是低位,右边是高位,因此写成:
0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x80