跳到内容
Shiny's Blog

逻辑与移位指令

上一章学了算术指令,它们做”数学运算”。这一章学逻辑指令移位指令,它们做”位运算”,包括 andorxornot 四条逻辑指令和 shlshrrolror 四条移位指令。

为什么位运算重要?

因为编译器最喜欢用它来优化代码。你在 x64dbg 里看到的很多”奇怪”指令,其实都是位运算。

逻辑指令

逻辑指令对操作数的每一位独立做运算,常用于掩码、设位、清零。

所有逻辑指令(and/or/xor)都会清零 CF 和 OF。ZF/SF/PF 根据结果正常更新。not 不改任何标志位。

真值表:and / or / xor 的规则

三种逻辑运算的区别就在这张表里:

ABand (A·B)or (A+B)xor (A⊕B)
00000
01011
10011
11110
  • and:两个都是 1 才是 1 → 用来过滤(保留想要的位,其余清零)
  • or:有一个是 1 就是 1 → 用来设位(把特定位设为 1)
  • xor:不同为 1,相同为 0 → 用来翻转(特定位取反)

后面的例子都是这张表的 32 位版本,对每一位独立套用同样的规则。

and:按位与

格式和 add 一样,寄存器、内存、立即数三种组合都支持:

and eax, 0xFF              ; 寄存器 & 立即数(只保留最低字节)
and eax, ebx               ; 寄存器 & 寄存器
and dword ptr [ebp-4], 0   ; 内存 & 立即数

逐位做与运算:两位都是 1,结果才是 1;否则为 0。最常用的场景是掩码操作,用 and 把不需要的位清零。操作数规则同第 6 章的 add

假设 EAX = 0x12345678,EIP = 00401000

and 掩码运算追踪与二进制拆解

影响标志位:ZF/SF/PF 根据结果更新,CF=0,OF=0(逻辑指令永远清零这两个)。

再来一个,假设 EAX = 0x00000000

and 全零运算追踪与 ZF 触发

or:按位或

格式同上,最常见用途是把某个特定的位设为 1:

or eax, 0x80               ; 寄存器 | 立即数(把第 7 位设为 1)
or eax, ebx                ; 寄存器 | 寄存器
or dword ptr [ebp-4], 1    ; 内存 | 立即数

逐位做或运算:只要有至少一个 1,结果就是 1。常用场景:设置特定位为 1。操作数规则同第 6 章的 add

假设 EAX = 0x00000041(ASCII 字符 ‘A’),EIP = 00401000

or 大小写转换追踪与二进制拆解

这是一个经典用法:or 第 6 位(0x20)可以把大写字母转成小写字母。

影响标志位:同 and,ZF/SF/PF 更新,CF=0,OF=0

xor:按位异或

最常见的两个用途:清零和翻转特定位:

xor eax, eax               ; 寄存器 ^ 寄存器(清零)
xor eax, 0x55              ; 寄存器 ^ 立即数(翻转特定位)

逐位做异或运算:两位不同为 1,相同为 0。xor 最常见的用法是清零,一个数和自己异或,结果一定是 0。操作数规则同第 6 章的 add

假设 EAX = 0xDEADBEEF,EIP = 00401000

xor 清零追踪与二进制拆解

为什么不用 mov eax, 0 机器码长度不同:

指令机器码字节数
xor eax, eax31 C02
mov eax, 0B8 00 00 00 005

编译器选 xor 是因为短 3 字节,而且现代 CPU 对 xor reg, reg 有专门的优化路径(识别为归零操作,消除寄存器依赖)。逆向时看到 xor reg, reg 就是在清零,这是程序开头最常见的指令之一。

编译器为什么要优化?

位运算指令通常比等价的算术指令更短、更快xor eax, eax 清零只要 2 字节,mov eax, 0 要 5 字节。后面会看到 shl eax, 1 乘 2 也比 imul 更短更快。更短的指令意味着更小的程序体积、更少的内存访问、更快的执行速度。编译器在生成代码时会自动选择最优的指令组合,所以你看到的汇编代码往往和源代码不是一一对应的。

xor 另一个用途是翻转特定位

假设 EAX = 0x00000055,EIP = 00401000

xor 按位取反追踪与二进制拆解

xor 一个全 1 的掩码,效果相当于 not,但 xor 会更新标志位而 not 不会。

影响标志位:同 and/or,ZF/SF/PF 更新,CF=0,OF=0xor eax, eax 之后 ZF 一定为 1。

硬件小知识

计算机底层其实做不了加减法,CPU 的加法器是由 XOR 门(求和)和 AND 门(进位)组合出来的。所以从硬件角度,逻辑运算才是”基础”,算术是”派生”。但对逆向来说,先学算术更容易上手,因为 add/sub 的含义比 and/or 更直观。

not:按位取反

单操作数指令,只能操作寄存器或内存,不能用立即数:

not eax                     ; 寄存器取反
not dword ptr [ebp-4]       ; 内存取反

not 是单操作数指令,每一位取反(0 变 1,1 变 0)。操作数可以是寄存器或内存,不能是立即数。

假设 EAX = 0x0000000F,EIP = 00401000

not 按位取反追踪与二进制拆解

not 不影响任何标志位,ZF、SF、CF、OF 全部保持原值。偶尔在加密算法或求补码时出现(not + add 1 = 取负数)。

移位和旋转指令

移位指令是编译器优化乘除法的首选工具。旋转指令在加密和哈希算法中常见。

shl / shr:逻辑左移 / 右移

编译器优化乘除法的核心工具,左移 = 乘,右移 = 除:

shl eax, 1                  ; 寄存器左移 1 位 = 乘以 2
shl eax, 2                  ; 寄存器左移 2 位 = 乘以 4
shr eax, 1                  ; 寄存器右移 1 位 = 除以 2
shr dword ptr [ebp-4], 3    ; 内存右移 3 位 = 除以 8

移位位数可以是立即数(1~31)或 CL 寄存器。目的操作数可以是寄存器或内存,不能是立即数。操作数规则和单操作数指令类似,只有一个操作数被修改,另一个是移位位数。

Tip

shl eax, 1add eax, eax 等价,都是乘以 2。但 shl 只需 2 字节,add 需要 2-3 字节,而且 shl 语义更明确。逆向中两种都会遇到。

编译器乘除法优化对照表

C 乘法优化结果C 除法(无符号)优化结果
x * 2shl eax, 1x / 2shr eax, 1
x * 4shl eax, 2x / 4shr eax, 2
x * 8shl eax, 3x / 8shr eax, 3
x * 16shl eax, 4x / 16shr eax, 4
x * 32shl eax, 5x / 32shr eax, 5

假设 EAX = 0x00000003,EIP = 00401000

shl/shr 移位追踪与二进制演化

影响标志位:ZF/SF/PF 根据结果更新。CF = 最后移出的那一位。OF 只在移 1 位时有意义(符号位是否变化)。

来看一个 CF 和 OF 都有变化的例子。假设 EAX = 0xC0000003(二进制最高两位为 11),EIP = 00401000

shl CF/OF 追踪与 32 位流向

逐行解释:

  • 第一行 shl eax, 10xC0000003 = 1100...0011,左移 1 位后最高位的 1 移进 CF,CF=1。结果 0x80000006 最高位仍然是 1,符号没变,OF=0
  • 第二行 shl eax, 10x80000006 = 1000...0110,左移 1 位后最高位的 1 移进 CF,CF=1。结果 0x0000000C 最高位变成 0,原来是负数现在变成了正数,正负号不一致 → OF=1

CF 的规则很简单:移出去的那一位就是 CF。OF 的判断也直白:移 1 位时,如果符号位发生了变化,就说明溢出了。

sal / sar:算术左移 / 右移

salshl 是同一条指令,关键区别在 sar,它处理有符号数的右移:

sal eax, 2                   ; 算术左移 2 位(与 shl 完全相同)
sar eax, 3                   ; 算术右移 3 位(高位补符号位)

salshl 是同一条机器码,没有任何区别。逆向时看到 shl 就行。

sarshr 的区别在于高位怎么补

  • shr(逻辑右移):高位补 0
  • sar(算术右移):高位补符号位(正数补 0,负数补 1)

假设 EAX = 0xF0000010(负数,最高位为 1),EIP = 00401000

先用 shr 右移 4 位:

shr 逻辑右移追踪

shr 不管原符号位,高位一律补 0。结果从负数变成了正数(0x0F000001),对于有符号数来说除法结果就错了。

再用 sar 右移同样的 4 位:

sar 算术右移追踪

sar 发现符号位是 1(负数),所以高位补 1。结果仍然是负数(0xFF000001),这才是正确的 -268435440 / 16 = -16777215

什么时候看到 sar:编译器对有符号变量做除以 2 的幂次优化时用。更多时候编译器会用乘以倒数的方式(第 6 章讲过的 imul + sar 组合)。

rol / ror:循环左移 / 右移

rol eax, 1                  ; 循环左移 1 位
ror eax, 4                  ; 循环右移 4 位

shl/shr 的区别:移出去的位不是丢弃,而是绕回到另一端

先看 rol eax, 1,假设 EAX = 0x80000001,EIP = 00401000

rol 循环左移追踪

再看 rol eax, 4,同样的初始值:

rol 循环左移 4 位追踪

对比:rol eax, 1 时 CF=1(移出的是 1),rol eax, 4 时 CF=0(最后一次移出的是 0)。循环移位多次时,CF 等于最后一次移出的位,不是第一次。

影响标志位:ZF/SF/PF 根据结果更新。CF = 移出的位。OF 只在移 1 位时有意义。

rol/ror 在普通应用程序中很少出现,但在以下场景中常见:

  • 加密算法(DES、AES 等的密钥调度)
  • 哈希函数(MD5、SHA 系列的轮函数)
  • 伪随机数生成器
  • CRC 校验计算

如果你在逆向中看到一段大量使用 rol/ror 的代码,多半是在做某种加密或哈希运算。

ror 示例:上面的 rol eax, 40x80000001 变成了 0x00000018。现在对 0x00000018ror,看看能不能变回去。EIP = 00401000

ror 循环右移 4 位追踪

果然变回了 0x80000001rorrol 的逆操作,rol eax, 4 后再做 ror eax, 4 就恢复原值。

指令对标志位影响总结表

指令ZFSFCFOF说明
add更新更新更新更新加法,全更新
sub更新更新更新更新减法,全更新
inc更新更新不变更新+1,保留 CF
dec更新更新不变更新-1,保留 CF
imul未定义未定义更新更新乘法,只有 CF/OF 可靠
idiv/div未定义未定义未定义未定义除法,标志位不可靠
and更新更新清零清零按位与
or更新更新清零清零按位或
xor更新更新清零清零按位异或
not不变不变不变不变按位取反,不改任何标志位
shl更新更新移出的位更新逻辑左移
shr更新更新移出的位更新逻辑右移
rol更新更新移出的位更新循环左移
ror更新更新移出的位更新循环右移

记忆口诀

  • not 什么都不改,独一无二的”安静”指令
  • inc/dec 不改 CF,保护循环中的进位标志
  • 逻辑三兄弟(and/or/xor)清零 CF 和 OF,每一位独立运算,不需要进位,所以没有进位也没有溢出,CF 和 OF 直接固定为 0
  • 移位指令的 CF = 移出的位,移出去什么 CF 就是什么

速查表

指令作用C 等价改什么改标志位?
add a, b加法a += ba是(全部)
sub a, b减法a -= ba是(全部)
inc a+1a++a是(不改 CF)
dec a-1a--a是(不改 CF)
imul a, b乘法a *= ba是(OF/CF)
idiv b有符号除EAX=商 EDX=余EDX:EAX不可靠
div b无符号除同上EDX:EAX不可靠
and a, b按位与a &= ba是(CF=0, OF=0)
or a, b按位或a |= ba是(CF=0, OF=0)
xor a, a清零a = 0a是(CF=0, OF=0)
not a按位取反a = ~aa
shl a, n左移a <<= na是(CF=移出位)
shr a, n右移a >>= na是(CF=移出位)
sal a, n算术左移同 shla是(CF=移出位)
sar a, n算术右移有符号 >>=a是(CF=移出位)
rol a, n循环左移a是(CF=移出位)
ror a, n循环右移a是(CF=移出位)

练习

  1. 初始 EAX = 0xDEADBEEF,ZF = 0。执行以下两条指令后,EAX 和 ZF 各是什么?

    xor eax, eax
    mov eax, 0
    参考答案

    执行 xor eax, eax 后 EAX = 0x00000000,ZF = 1。 执行 mov eax, 0 后 EAX = 0x00000000(没变),但 ZF 仍为 1,因为 mov 不改标志位。

    两条指令最终效果相同(EAX = 0),但 xor 会设 ZF=1,mov 不碰 ZF。

  2. 初始 EAX = 0xABCD1234。执行以下指令后,EAX 的值是什么?

    and eax, 0x0000FFFF
    参考答案

    EAX = 0x00001234

    0x0000FFFF 是一个掩码,高 16 位全是 0,低 16 位全是 1。AND 之后高 16 位清零,低 16 位保留。这就是”取低字(低 16 位)“的标准操作。

  3. 初始 EAX = 0x00000007。执行以下指令后,EAX 的值是什么?等于 7 * 16 吗?

    shl eax, 4
    参考答案

    EAX = 0x00000070 = 112 = 7 * 16。正确。

    左移 4 位 = 乘以 2^4 = 乘以 16。二进制验证:

    7     = 0000 0000 0000 0000 0000 0000 0000 0111
    << 4  = 0000 0000 0000 0000 0000 0000 0111 0000  = 112 (0x70)
  4. 初始 EAX = 0x0000000E(二进制 ...1110)。执行以下指令后,EAX 和 CF 各是什么?

    rol eax, 1
    参考答案

    EAX = 0x0000001C,CF = 0

    0x0E = ...0000 1110
    ROL 1: 最左边的 0 移出去绕回到最低位
    结果 = ...0001 1100 = 0x1C = 28
    CF = 移出的位 = 0

    0x0E 的最高有效位(bit 3)是 1,但 32 位视角下 bit 31 是 0,所以移出的是 0,CF=0。效果等于 0x0E * 2 = 0x1C = 28


目录