常用汇编指令
Little_YangYang

汇编指令概述

X86的汇编指令一般由一个opcode操作码和0到多个operand操作数组成,大多数指令包含两个操作数,即一个目的操作数和一个源操作数

基本组成

  1. 操作码(Opcode)

    • 操作码是指令的核心部分,指定了处理器要执行的具体操作。常见的操作码包括:MOV(数据传送)、ADD(加法)、SUB(减法)、MUL(乘法)、DIV(除法)等。
  2. 操作数(Operands)

    • 操作数是指令的参数或数据,通常有两个操作数,即目的操作数和源操作数。根据指令的类型,操作数可以是寄存器、内存地址、立即数或它们的组合。

    • 目的操作数:指执行完操作后存储结果的地方(通常是寄存器或内存位置)。

    • 源操作数:指进行操作的输入数据。

x86汇编中操作数的类型

  1. 寄存器(Register)

    • 操作数可以是寄存器,例如 AX, BX, CX, DX 等寄存器。寄存器的使用通常比内存操作快。
    • 示例:MOV AX, BX 表示将寄存器 BX 的值复制到寄存器 AX
  2. 立即数(Immediate Value)

    • 操作数可以是立即数,也就是一个固定的常量值,直接嵌入到指令中。
    • 示例:MOV AX, 10 表示将数值 10 放入寄存器 AX 中。
  3. 内存地址(Memory Address)

    • 操作数可以是一个内存地址,指定操作的数据存放在某个特定的内存位置。
    • 示例:MOV AX, [0x1000] 表示将内存地址 0x1000 的数据加载到 AX 寄存器中。
  4. 偏移地址(Displacement Addressing)

    • 在操作内存时,常常使用偏移地址,通过寄存器加上一个常量来计算实际的内存地址。
    • 示例:MOV AX, [BX+4] 表示将 BX 寄存器加上 4 处的内存地址中的值传送到 AX 中。

常用汇编指令

整数加减指令

X86汇编使用ADD指令进行整数的加法运算,该指令有两个操作数,第一个操作数位目的操作数,第二个操作数为源操作数,ADD指令将两个操作数相加,结果存储在目的操作数中。源操作数可以为寄存器、内存或立即数,目的操作数需要满足可写的条件,所以只能是寄存器或内存,两个操作数不能同时为内存。

1
2
3
ADD EAX, EBX    ; EAX = EAX + EBX
ADD EAX, 16 ; EAX = EAX + 16
ADD EAX, 1 ; EAX = EAX + 1

减法指令使用SUB:将源操作数从目的操作数中减去。

1
SUB EAX, EBX    ; EAX = EAX - EBX

ADD和SUB在内的很多汇编指令可以接受不同大小的参数,可以进行不同位数的加减法。

数据传输指令

在x86架构中,数据传输指令,尤其是MOV指令,是最常用的指令之一。通过MOV指令,可以在寄存器之间、寄存器与内存之间、立即数与寄存器或内存之间传输数据。MOV指令遵循的基本格式是将源操作数复制到目的操作数中。

MOV指令的基本形式

1
2
3
MOV AX, BX         ; 将BX中的数据复制到AX
MOV [0x2000], AX ; 将AX的内容存储到内存地址0x2000
MOV CX, 5 ; 将立即数5赋值给CX寄存器

在这三种例子中,源操作数可以是寄存器、内存地址或立即数,而目的操作数可以是寄存器或内存地址。MOV指令会将源操作数的值复制到目的操作数中,源操作数保持不变。

内存地址中的位移计算

在MOV指令中,内存地址可以直接通过位移(displacement)来计算,或者通过其他寄存器和位移的组合来生成地址。以下是常见的内存寻址模式,包括位移操作的示例。

  1. 直接位移(Displacement Addressing)
    • 位移是一个固定的常量值,表示内存地址的偏移量。
    • 示例:
      1
      2
      MOV AX, [0x2000]   ; 访问内存地址0x2000处的数据,将其复制到AX
      MOV [0x2000], BX ; 将BX中的数据复制到内存地址0x2000
  2. 基址寻址(Base Addressing)
    • 通过基址寄存器(如BXBP)加上位移值,计算出内存地址。
    • 示例:
      1
      2
      MOV AX, [BX+0x10]   ; 将BX寄存器加上位移0x10,访问该地址的内存数据,并将其复制到AX
      MOV [BP-0x20], CX ; 将CX中的数据存储到BP寄存器减去0x20的位置
  3. 变址寻址(Indexed Addressing)
    • 通过变址寄存器(如SIDI)加上位移值计算出内存地址。
    • 示例:
      1
      2
      MOV AX, [SI+0x10]   ; 将SI寄存器加上位移0x10,访问该地址的数据并复制到AX
      MOV [DI-0x20], BX ; 将BX中的数据存储到DI寄存器减去0x20的位置
  4. 基址-变址寻址(Base-Index Addressing)
    • 结合基址寄存器和变址寄存器的值,并加上一个位移,计算出内存地址。
    • 示例:
      1
      MOV AX, [BX+SI+0x10]   ; 基址BX加上变址SI,再加上位移0x10,计算出内存地址,并将数据传送到AX
  5. 比例变址寻址(Scaled-Index Addressing)
    • 使用比例因子乘以变址寄存器的值,并加上基址寄存器或位移来计算内存地址。
    • 示例:
      1
      MOV AX, [BX+SI*2+0x10]   ; 基址BX加上SI寄存器乘2,再加上位移0x10,访问该内存地址,并将数据复制到AX

位移(Displacement)的类型

位移(displacement)可以是一个8位、16位或32位的值。它表示与某个基址或变址寄存器的偏移量,通常用于表示结构体字段、数组元素或函数局部变量的地址。

  • 8位位移:当偏移量较小时,使用8位位移可以减少指令长度。
  • 16位位移:用于较大偏移量。
  • 32位位移:主要用于x86-64架构中更大内存空间的访问。

示例:复杂的内存访问

以下示例展示了使用基址、变址和位移的组合来计算复杂的内存地址:

1
MOV AX, [BX+SI*2+0x20]   ; 基址BX加上SI的2倍,再加上0x20的位移,计算出内存地址并将数据复制到AX

这个指令会先将SI的值乘以2,然后与BX相加,最后再加上0x20,得出的结果就是内存地址。处理器会从这个地址读取数据,并将其放入寄存器AX中。

通过这种方式,MOV指令可以灵活地处理内存访问,并支持多种寻址模式,以便更高效地管理数据传输和计算。

逻辑运算指令

AND:按位与操作。

1
AND AX, BX    ; AX = AX AND BX

OR:按位或操作。

1
OR AX, BX     ; AX = AX OR BX

XOR:按位异或操作。

1
XOR AX, BX    ; AX = AX XOR BX

入栈和出栈指令

在x86架构中,入栈和出栈指令PUSHPOP用于操作栈,尽管它们并不直接以栈指针寄存器(ESP)为操作数,但它们隐式地修改ESP的值。这些指令通过修改栈指针来管理数据的进出栈操作。

  • PUSH指令将数据压入栈,隐式递减ESP,并将数据存储到新的栈顶位置。
  • POP指令从栈中弹出数据,隐式增加ESP,并将栈顶数据存储到指定的寄存器或内存位置。

PUSH指令的工作机制

  1. 栈的增长方向
    • 在x86架构中,栈是向下增长的,也就是说,随着数据被压入栈,ESP寄存器的值会减少。
  2. PUSH指令的步骤
    • 第一步ESP向下移动(递减)一个栈帧的大小,以为即将入栈的数据腾出空间。栈在x86的32位模式下是以4字节为单位的,所以在32位环境下,ESP会减少4字节。
    • 第二步:将源操作数的值复制到ESP指向的内存地址,即新的栈顶位置。

PUSH指令的内部过程

假设栈指针ESP当前指向地址0x1000,且在32位模式下:

  1. PUSH AX的第一步会将ESP减去4(因为每次压入栈的数据为4字节),此时ESP变为0x0FFC
  2. 然后,AX中的值会被复制到0x0FFC这个内存地址处,这就是ESP现在指向的地址。
1
PUSH AX    ; 将AX中的数据压入栈

等价于

1
2
SUB ESP, 4      ; 将ESP的值减去4,栈指针向下移动
MOV [ESP], AX ; 将AX中的值存储到当前ESP指向的地址

ESP的隐式修改

如上所述,PUSH指令虽然不直接以ESP为操作数,但它会隐式修改ESP的值。执行PUSH指令时,ESP会递减以提供新的栈空间,然后将源操作数存储到新的栈顶。

POP指令的工作机制

PUSH相对,POP指令从栈中弹出数据,并将其存储到目的操作数中。POP指令的步骤如下:

  1. 第一步:将当前ESP指向的内存地址中的值复制到目的操作数中。
  2. 第二步:将ESP增加4(在32位模式下),使栈指针指向下一个栈帧。
1
POP BX    ; 将栈顶的数据弹出并存入BX

等价于

1
2
MOV BX, [ESP]   ; 将ESP指向的内存中的数据存储到BX
ADD ESP, 4 ; 将ESP的值加4,栈指针向上移动

分支跳转指令

在x86架构中,分支跳转指令用于修改指令指针寄存器 EIP(扩展指令指针寄存器) 的值,以实现程序的控制流跳转。由于不能直接对 EIP 进行修改,因此必须使用专门的跳转指令来实现程序的控制流变更。这类跳转指令可以使程序跳转到指定的代码地址或根据条件进行跳转。

无条件跳转指令 (JMP)

无条件跳转指令 JMP 是最基础的跳转指令,它会立即修改 EIP 的值,从而实现跳转到指定的代码位置。它有多种使用方式,可以通过标签、立即数、寄存器或内存地址来确定跳转的目标位置。

JMP指令的常见形式

  1. 直接跳转到标签(直接地址跳转)
1
JMP label    ; 跳转到代码中的某个标签

在汇编代码中,label 是代码的某个位置的标记,执行 JMP label 时,程序将无条件地跳转到该标签对应的指令位置,修改 EIP 的值使其指向标签处。

  1. 立即数跳转(绝对跳转)
1
JMP 32       ; 跳转到内存地址32处

这里的 32 是一个立即数,表示内存中的某个绝对地址。程序执行时将 EIP 设置为该地址,从而跳转到内存地址 32 处。

  1. 基于寄存器的跳转(间接跳转)
1
JMP [EAX+32] ; 跳转到寄存器EAX加上偏移量32处的内存地址

在这种形式下,JMP 指令会先计算内存地址 EAX + 32,然后跳转到这个地址。这里 EAX 是一个寄存器,32 是一个偏移量,这种跳转方式称为间接跳转。

条件跳转指令

除了无条件跳转之外,x86架构还提供了一系列条件跳转指令,这些指令根据某些条件是否满足来决定是否修改 EIP 的值。条件跳转指令通常依赖于标志寄存器(EFLAGS)中的标志位,这些标志位是在某些操作(如算术运算)执行后被设置的。

以下是一些常见的条件跳转指令:

  1. JE / JZ(Jump if Equal / Jump if Zero)
    • 当零标志位被设置时跳转,即上一次比较的结果相等或为零时跳转。
    • 示例:
      1
      2
      JE label    ; 如果相等,则跳转到label处
      JZ label ; 如果为零,则跳转到label处
  2. JNE / JNZ(Jump if Not Equal / Jump if Not Zero)
    • 当零标志位未设置时跳转,即上一次比较的结果不相等或不为零时跳转。
    • 示例:
      1
      2
      JNE label   ; 如果不相等,则跳转到label处
      JNZ label ; 如果不为零,则跳转到label处
  3. JG / JNLE(Jump if Greater / Jump if Not Less or Equal)
    • 当操作数大于另一个操作数时跳转,常用于有符号数比较。
    • 示例:
      1
      JG label    ; 如果大于,则跳转到label处
  4. JL / JNGE(Jump if Less / Jump if Not Greater or Equal)
    • 当操作数小于另一个操作数时跳转,常用于有符号数比较。
    • 示例:
      1
      JL label    ; 如果小于,则跳转到label处
  5. JGE(Jump if Greater or Equal)
    • 当操作数大于或等于另一个操作数时跳转。
    • 示例:
      1
      JGE label   ; 如果大于或等于,则跳转到label处
  6. JLE(Jump if Less or Equal)
    • 当操作数小于或等于另一个操作数时跳转。
    • 示例:
      1
      JLE label   ; 如果小于或等于,则跳转到label处

近跳转与远跳转

在x86架构中,跳转可以是近跳转(near jump)或远跳转(far jump)。

  • 近跳转(Near Jump):仅修改当前代码段中的 EIP。通常用于程序内部的跳转,目标地址是相对于当前地址的短距离范围内(通常是-128到+127字节范围内)。
    • 示例:
      1
      JMP short label    ; 短跳转
  • 远跳转(Far Jump):不仅修改 EIP,还修改代码段寄存器(CS),允许跳转到不同的段内的代码。用于在不同段之间的跳转。
    • 示例:
      1
      JMP far label    ; 远跳转

循环跳转

循环跳转指令也是控制流的常见形式,典型指令包括:

  1. LOOP
    • LOOP 指令每次执行时将寄存器 CX 减1,如果 CX 不为零,则跳转到指定标签处。
    • 示例:
      1
      LOOP label    ; 如果CX不为0,则跳转到label处
  2. LOOPZ / LOOPNZ
    • LOOPZ:当 CX 不为零且零标志位设置时跳转。
    • LOOPNZ:当 CX 不为零且零标志位未设置时跳转。
    • 示例:
      1
      2
      LOOPZ label    ; 如果CX不为0且零标志设置,则跳转到label
      LOOPNZ label ; 如果CX不为0且零标志未设置,则跳转到label

过程调用指令

在x86架构中,过程调用指令用于实现函数调用返回。这些指令通常与栈配合使用,用于保存调用者的上下文、传递参数、保存返回地址等。最常用的过程调用指令包括 CALLRET,它们通过隐式修改 EIP 寄存器并管理栈指针 ESP 来实现函数调用和返回。

CALL指令

CALL 指令用于调用一个子过程(函数)。它的作用是: 1. 将当前指令的下一条指令地址(返回地址)压入栈,从而在函数执行完毕后可以返回到调用位置。 2. 跳转到目标地址(函数入口),从而执行被调用的函数。

CALL指令的使用形式

  1. 直接调用(Direct Call)
    • 直接调用某个标签或地址的函数。
    • 示例:
      1
      2
      CALL label    ; 调用名为label的子过程
      CALL 0x1234 ; 调用地址为0x1234的子过程
    • 在直接调用中,函数的入口地址是明确的,它可以是标签、立即数地址等。
  2. 间接调用(Indirect Call)
    • 间接调用时,函数的入口地址存储在寄存器或内存中。
    • 示例:
      1
      2
      CALL [EAX]    ; 调用EAX寄存器中存储的地址
      CALL [EBX+4] ; 调用内存地址EBX+4处的函数

CALL指令的工作原理

  • 当执行 CALL 指令时,当前指令指针 EIP 的值(即下一条指令的地址)会被压入栈中,这个值就是调用函数返回时的地址。
  • 然后,EIP 被设置为目标地址(函数的入口地址),程序控制流跳转到该函数并开始执行。

假设当前指令的地址是 0x1000,函数的入口地址是 0x2000,当执行 CALL 0x2000 时: 1. 当前 EIP 的值(0x1004,下一条指令的地址)会被压入栈。 2. EIP 被设置为 0x2000,程序跳转到函数入口 0x2000 处执行。

RET指令

RET 指令用于从子过程返回。它的作用是: 1. 从栈中弹出返回地址,将其恢复到指令指针寄存器 EIP,从而返回到调用过程中的位置。 2. 恢复执行调用函数后的指令

RET指令的使用形式

  1. 无参返回(Simple Return)
    • 弹出栈顶的返回地址,将其加载到 EIP,继续执行主程序。
    • 示例:
      1
      RET    ; 从子过程返回
  2. 带参返回(Return with Stack Adjustment)
    • 在返回时,还可以调整栈指针 ESP 的值,移除函数调用时压入栈的参数。
    • 示例:
      1
      RET 8    ; 返回并弹出栈上的8字节
    • RET 8 表示返回时,同时调整 ESP 指针,移除8字节(通常用于清理调用时传递的参数)。

RET指令的工作原理

  • 当执行 RET 指令时,栈顶的值(调用时保存的返回地址)会被弹出并加载到 EIP 中,这个地址就是返回时程序应继续执行的位置。
  • 然后,程序会跳转到该地址并继续执行。

例如,在调用函数返回时: 1. ESP 指向的栈顶值是 0x1004(函数调用时保存的返回地址)。 2. 执行 RET 后,EIP 会被设置为 0x1004,从而返回到函数调用后的位置继续执行。

CALL与RET的配合使用

CALLRET 是成对使用的,通常表现为:

  1. 执行 CALL 指令,保存返回地址并跳转到被调用函数。
  2. 被调用函数执行其代码,并在结束时通过 RET 指令返回调用处。
1
2
3
4
5
CALL myFunction   ; 调用myFunction
; 执行其他代码
myFunction:
; 函数代码
RET ; 从myFunction返回

栈帧与函数调用

在x86架构中,函数调用常常涉及到栈帧的管理。栈帧是一段用于保存函数局部变量、参数和返回地址的栈空间。EBP(基址指针寄存器)常被用于管理栈帧,配合 ESP 管理函数调用时的数据。

典型的函数调用过程如下:

  1. 调用者保存环境

    • 调用者将当前环境(如某些寄存器的值)压入栈。
  2. CALL指令调用函数

    • 执行 CALL 指令,压入返回地址并跳转到被调用函数。
  3. 被调用函数保存栈帧

    • 被调用函数会保存调用者的栈帧,并为自身创建新的栈帧。常用的操作如下:
      1
      2
      3
      PUSH EBP       ; 保存调用者的EBP
      MOV EBP, ESP ; 设置新的EBP,指向当前栈顶
      SUB ESP, size ; 为局部变量腾出空间
  4. 函数执行

  5. 函数清理栈帧并返回

    • 函数执行结束后,恢复调用者的栈帧,并返回:
      1
      2
      3
      MOV ESP, EBP   ; 恢复ESP为调用前的状态
      POP EBP ; 恢复EBP
      RET ; 返回调用处

示例:典型函数调用与返回

1
2
3
4
5
6
7
8
9
10
11
12
13
main:
; 准备调用子过程
CALL subroutine
; 返回后继续执行

subroutine:
PUSH EBP ; 保存EBP
MOV EBP, ESP ; 创建新的栈帧
SUB ESP, 16 ; 为局部变量腾出16字节空间
; 执行子过程代码
MOV ESP, EBP ; 恢复ESP
POP EBP ; 恢复EBP
RET ; 返回到调用处