汇编:跟踪指令笔记

这是我阅读《汇编语言:基于 Linux 环境 第 3 版》的笔记

mov 指令

mov 指令将数据从源复制到目的地,一旦复制到目的地后数据不会从源消失,而不是实际的“移动”。

1
mov eax, 1

有两个操作数 eax 和 1。
在汇编中机器指令的第一个操作数是目标操作数,第二个操作数是源操作数。
代码中源操作数(字面常量 1)被复制到目标操作数 eax 中。

1
mov eax, 42h

立即数,只有源操作数可以是立即数。目标操作数是数据要取得地方。立即数由字面常量组成。

1
mov eax, 'WXYZ'

运行后寄存器 eax 数据为 0x5a595857。这个值与 WXYZ 等值。因为 x86 是小端字节序,所以从后往前放。

1
2
3
4
5
    mov ax, 067feh ; 16 位十六进制值被移到寄存器 ax 中
    mov bx, ax     ; ax 复制到 bx 中
    mov cl, bh     ; bx 的高字节部分被移到 cx 的低字节部分
    mov ch, bl     ; bx 的低字节部分被移到 cx 的高字节部分
    xchg cl, ch    ; 交换两个操作数的值

一系列的寄存器操作。

内存数据

立即数是内置在机器指令本身内部的数据,寄存器数据是存放在某个位于 CPU 内部寄存器的数据,而内存数据是存储在分配给某个程序的系统内存中的某个 32 位内存地址处的数据。

如果想要指定的数据是位于寄存器中的内存地址中的值,而不是寄存器本身中的数据,需要在寄存器名字的两边加上方括号(解引用)。比如想要 EBX 中的内存地址的字移到寄存器 EAX 中,应该使用以下指令:

1
mov eax, [ebx]

还可以在寄存器括号中添加字面常量。比如以下指令:

1
mov eax, [ebx + 16]

这里将内存中特定地址的 32 位数据加载到 eax 寄存器中。其中 [ebx + 16] 中的 +16 是立即数偏移量,表示 ebx 指向的地址基础上向后偏移 16 字节。
甚至可以添加两个通用寄存器甚至是多个叠加:

1
mov eax, [ebx+ecx+11]

混淆数据和它的地址

1
2
hello db 'Hello, World!', 0xA
mov ecx, hello

在汇编中变量名代表的是地址,而不是数据。这里 mov 指令实际复制的是 hello 的地址。
而如果加上了方括号:

1
mov ecx, [hello]

这时候所作的事从 hello 代表的内存地址中取出前 32 位数据,从最低有效字节开始将其加载到 ecx 中,此时 ecx 将保存 Hell 四个字符.
如果只想要其中一个单独字符,只想要加载到一个字节大小的容器(寄存器)中。可以通过 AL 来寻址 EAX 的最低有效字节:

1
mov al, [hello]

除此之外可以用 ax 指代 EAX 中的两个字节

1
mov [hello], byte 'G '

这里将 hello 指向地址的数据修改为 G,使用 byte 关键字强制指令按字节操作。会忽略空格

CPU 标志位

CPU 在执行算术运算和逻辑运算时,会根据运算结果设置一些状态标志,这些标志存储在 eflag 寄存器中。标志位在程序控制、调试和异常处理中起着至关重要的作用。
EFlags 寄存器中的每一位标志都有一个由两个或三个字幕组成的符号,大多数程序员通过这些符号来了解它们。

  • CF 进位标志 (Carry Flag)
    用于指示无符号数运算时是否发生了进位或借位。
  • PF 奇偶标示 (Parity Flag) 用途: 用于奇偶校验
    • 低 8 位结果中 1 的个数是 偶数,PF = 1;
    • 低 8 位结果中 1 的个数是 奇数,PF = 0。
  • AF 辅助进位标示 (Auxiliary Carry Flag)
    主要用于 BCD(十进制编码)运算。
  • ZF 零标志 (Zero Flag)
    用于指示运算结果是否为零。
    • 结果为 0,则 ZF = 1;
    • 结果不为 0,则 ZF = 0。
  • SF 符号标志 (Sign Flag)
    用于指示运算结果的符号,它的值等于运算结果的最高位
    • 结果的最高位为 1(负数),则 SF = 1
    • 结果的最高位为 0(正数或零),则 SF = 0
  • TF 陷阱标志 (Trap Flag)
    • 当 TF = 1 时,CPU 每执行一条指令就会触发一次调试中断。
    • 用途: 主要用于单步调试
  • IF 中断标志 (Interrupt Flag)
    • IF = 1 时,CPU 允许外部 可屏蔽中断(IRQ) 发生。
    • IF = 0 时,CPU 屏蔽中断,用于关键代码执行期间。
  • DF 方向标志 (Direction Flag)
    控制字符串操作的方向
    • DF = 0 从低地址到高地址(自增模式)
    • DF = 1 从高地址到低地址(自减模式)
    • 用途: 在 MOV 等字符串指令中决定数据的存取顺序
  • OF 溢出标志 (Overflow Flag)
    • 当对一个有符号整数量的算术运算操作结果过于庞大,以至于不能适合原来的操作数时,溢出标志 OF 被设置。当进位标示被标记时,溢出标志通常也被设置。
  • ID

INC 和 DEC

INC 和 DEC 将一个操作数分别加 1 和减 1。他们均只需要一个操作数。

1
2
3
4
mov eax, 0x0fffffff
mov ebx, 0x02d
dec ebx
inc eax

dec 将 ebx 的值减 1,上例中从 0x02d 变成 0x02c。
inc 将 eax 的值加 1,上例中从 0x0fffffff 变成 0x10000000

标志跳转

条件转移指令是测试。它们测试某个标志的值,要么继续往下走,要么跳转到程序的其他部分。
最简单的条件转移指令是 JNZ。如果不为零则跳转。
JNZ 指令测试零标志(ZF)的值。如果 ZF 已经被设置(即等于1),则什么也不发生。但是如果 ZF 没有被设置,那么执行到程序中一个新目的地

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
section .data
    Snippet db "KANGAROO"
    len equ $ - Snippet

section .text
    global _start

_start:
        nop
        mov ebx, Snippet
        mov eax, 8
DoMore: add byte [ebx], 32 ; 给 EBX 寄存器指向的内存地址中的一个字节数据加上数值 32
                           ; 这里的作用是把 K 变成 k
        inc ebx ; 指向下一个字符
        dec eax ; 自减计数器
        jnz DoMore
        nop

section .bss 

这个汇编程序仅在 x86 下可以正常运行调试,64 位会直接段错误。
代码执行结束后 Snippet 下字母全部转换成小写。

1
add byte [ebx], 32

把 ebx 放到方括号内,引用的是 Snippet 的内容。byte 大小说明符告诉 NASM 我们只是将一个字节写到 EBX 中存放的内存地址中。

补码和 NEG

1
2
3
        mov eax, 5
DoMore: dec eax
        jmp DoMore

这段代码不断进行 eax 自减操作。
通过查看 eax 的值,可以看到从最初的 0x5 随着自减到 -1 变成 0xffffffff。
这就是补码。

1
2
3
mov eax, 42  ; 0x2a
neg eax      ; 0xffffffd6
add eax, 42  ; 0x0

neg eax 将 42 变成 -42。 -42 用反码表示。 再将 eax 加上 42 可以发现变成 0 了。

符号拓展和 MOVSX

因为寄存器的最高位作为符号位,但是比如 16 位寄存器的数据迁移到 32 位,此时用 mov 指令只纯粹按位复制,显然这不是我们想要的数据。

1
2
mov ax, -42
mov ebx, eax

在这里,对 ax 赋值为 -42 但是在 kdbg 中,其对应的寄存器 rax 数据却为 65494。我们应该使用 MOVSX 来进行处理。

隐式操作数和 MUL

在 x86 指令集中,有两套乘法和除法指令。一组是 MUL 和 DIVV,用于处理无符号计算。另一组是 IMUL 和 IDIV,用于处理有符号计算。 MUL 将两个值相乘,然后返回一个结果。

1
2
3
mov al, 100
mov bl, 10
mul bl

上述代码后 ral 值变成 1000,也就是 100 * 10。

机器指令 显式操作数(因子1) 隐式操作数(因子2) 隐式操作数(结果)
MUL r/m8 AL AX
MUL r/m16 AX DX and AX
MUL r/m32 EAX EDX and EAX

一但结果需要不止 16 位,D 系列寄存器就被征用来存放计算结果的高序位部分。如果将两个 16 位值相乘得到结果为 02A456Fh,那么寄存器 AX 中会包含 0456Fh,而 DX 寄存器中则存放 02Ah。

mul 指令的操作数不能为立即数。

MUL 和进位标志

当乘法结果的值溢出低序位寄存器时,乘法指令将设置进位标志(CF)。经过一次 MUL 指令后,如果发现 CF 被设置为 0,则可以忽略高序位寄存器,如果高序位寄存器中存在有效数字则 CF 为 1。

1
2
3
mov eax, 0ffffffffh
mov ebx, 03b72h
mul ebx

这段汇编在执行 mul 操作前在 gdb 下通过 info reg eflags 输出仅有 [IF] 标志符。说明只有中断标识被设为 1。而当执行 mul 后,eflags 的值中为 1 的标志符有: [ CF(进位标志) PF(奇偶标志) SF(符号标志) IF(中断标志) OF(溢出标志) ] 。
这里,CF 主要告诉你的是计算结果的高位部分存在有效数字,而这些有效数字存放在用于 32 位乘法运算的 EDX 寄存器中。

使用 DIV 实现无符号除法

DIV 将一个值和被另外一个值除,然后得出一个商和一个余数,这是整数运算。而要执行小数计算就要使用 x86 的数学处理器,比较复杂。
DIV 指令不影响任何标志。

一些符号

r8 = AL AH BL BH CL CH DL DH
r16 =AX BX CX DX BP SP SI DI
sr =CS DS SS ES FS GS
r32 = EAX EBX ECX EDX EBP ESP ESI EDI
m16=16位内存数据
i8=8位立即数
i32= 32位立即数
d16 =16位有符号位移
m8 =8位内存数据
m32 = 32位内存数据
i16=16位立即数 d8 =8位有符号位移
d32= 32位无符号位移
大小说明符: BYTE WORD DWORD

NEG 求补指令

表示 正数 负数
原码 数据本身的二进制表示(符号位为 0) 数据本身的二进制表示(符号位为 1)
反码 和原码一样 符号位不变,其余各位按位取反
补码 和原码一样 反码加一

neg reg/mem 即用 0 减操作数(把操作数按位取反,末位加 1),并将求得的结果存入指定的寄存器或内存单元

1
2
mov al, 0x64
neg al  ; al=0x9c

0x64 为 0b0110 0100
按位取反 0b1001 1011
末位加1 0b1001 1100

参考资料

  1. 汇编语言:基于 Linux 环境 第 3 版 清华大学出版社
  2. 汇编求补指令neg
萌ICP备20241614号