第九章 汇编指令精讲(二):逻辑运算、移位与跳转
各位同学,咱们接着往下聊。上一章我们把算术指令过了一遍,今天要讲的这几个指令,说实话,在逆向工程里出现的频率比加减乘除还要高。为什么?因为逻辑运算和移位操作,是底层优化的核心手段,也是很多加密算法的基石。
我个人习惯把这一章的内容分成三大块:逻辑运算、移位操作、以及控制流跳转。这三块东西,你如果吃透了,看反汇编代码的速度能提升一倍。
9.1 逻辑运算指令:AND、OR、XOR、NOT
逻辑运算,说白了就是按位操作。CPU 不会直接处理「真」和「假」,它只认 0 和 1。这四个指令,就是用来操作这些比特位的。
9.1.1 AND(按位与)
AND 的规则很简单:两个位都是 1,结果才是 1。否则就是 0。
; 示例:清空特定位
mov al, 0b11001100
and al, 0b11110000 ; al = 0b11000000
我在项目中遇到过这样一个场景:一个驱动程序的标志位寄存器,低 4 位表示状态,高 4 位是保留位。我想读取状态,就必须把高 4 位屏蔽掉。一条 AND 指令就搞定了。
实用技巧:AND 指令常用于「位清零」。如果你想保留某些位,就把对应的掩码位设为 1;想清零的位,掩码设为 0。
9.1.2 OR(按位或)
OR 的规则:只要有一个是 1,结果就是 1。两个都是 0 才是 0。
; 示例:设置特定位
mov al, 0b00001111
or al, 0b11110000 ; al = 0b11111111
OR 最常见的用途是「位置位」。你想想看,如果你想把某个寄存器的第 3 位设为 1,又不想动其他位,OR 一个 0b00001000 就行了。
9.1.3 XOR(按位异或)
XOR 的规则:两个位相同为 0,不同为 1。这个指令非常有意思。
; 示例:寄存器清零(经典用法)
xor eax, eax ; 比 mov eax, 0 更高效
; 示例:翻转特定位
mov al, 0b11110000
xor al, 0b00001111 ; al = 0b11111111
我曾经在分析一个恶意软件样本时,看到它用 XOR 来解密字符串。它把加密后的字节流和某个固定密钥做 XOR,还原出明文字符串。这种手法在壳和加解密代码里太常见了。
注意:XOR 同一个数两次,会得到原数。这是对称加密的基础。很多反调试技巧也利用了这个特性。
9.1.4 NOT(按位取反)
NOT 最简单:0 变 1,1 变 0。单操作数指令。
mov al, 0b11110000
not al ; al = 0b00001111
NOT 在补码运算中经常出现。比如求一个数的相反数,可以用 NOT 加 ADD 1 来实现。
9.2 移位指令:SHL 与 SHR
移位指令,就是把二进制位向左或向右移动。左移一位相当于乘以 2,右移一位相当于除以 2。但要注意,这是针对无符号数的。
9.2.1 SHL(逻辑左移)
mov al, 0b00000001
shl al, 1 ; al = 0b00000010,相当于乘以2
shl al, 2 ; al = 0b00001000,相当于乘以8
编译器特别喜欢用 SHL 来代替乘法。为什么?因为 SHL 只需要一个时钟周期,而乘法指令可能要好几个周期。你反编译优化过的代码时,经常会看到这种模式。
9.2.2 SHR(逻辑右移)
mov al, 0b10000000
shr al, 1 ; al = 0b01000000,相当于除以2
shr al, 3 ; al = 0b00001000,相当于除以8
嗯,这里要注意:SHR 是逻辑右移,高位补 0。对于有符号数的算术右移,用的是 SAR 指令。很多新手在这里翻过车。
核心要点:左移 SHL 相当于乘以 2^n,右移 SHR 相当于除以 2^n(无符号)。编译器优化时,会用移位代替乘除法。
9.3 比较与跳转指令:CMP、JMP、Jcc
程序不是一条直线跑到底的。条件判断、循环、分支,都依赖比较和跳转指令。
9.3.1 CMP(比较指令)
CMP 本质上就是做一次减法,但不保存结果,只影响标志位。
mov eax, 10
mov ebx, 20
cmp eax, ebx ; 计算 eax - ebx,设置标志位
CMP 之后,ZF(零标志)、CF(进位标志)、SF(符号标志)、OF(溢出标志)都会被更新。后续的跳转指令就是根据这些标志位来做决策的。
9.3.2 JMP(无条件跳转)
JMP 就是「跳到那里去」,没有任何条件。
jmp label_name ; 直接跳转
; 或者
jmp eax ; 间接跳转,跳到 eax 指向的地址
JMP 在逆向中经常出现在 switch-case 结构里。编译器会把多个 case 的地址放到一个跳转表中,然后用 JMP 间接跳转。这种代码看起来像一团乱麻,但摸清规律后很好解。
9.3.3 Jcc(条件跳转)
Jcc 是一族指令,cc 代表 condition code。常用的有:
| 指令 | 条件 | 标志位 | 说明 |
|---|---|---|---|
| JE / JZ | 相等 / 为零 | ZF=1 | CMP 结果相等时跳转 |
| JNE / JNZ | 不相等 / 不为零 | ZF=0 | CMP 结果不等时跳转 |
| JG / JNLE | 大于(有符号) | ZF=0 且 SF=OF | 有符号数比较 |
| JL / JNGE | 小于(有符号) | SF≠OF | 有符号数比较 |
| JA / JNBE | 高于(无符号) | CF=0 且 ZF=0 | 无符号数比较 |
| JB / JNAE | 低于(无符号) | CF=1 | 无符号数比较 |
我曾经在分析一个网络协议栈时,看到一段代码用 CMP 和 JB/JAE 来判断数据包长度是否越界。这种模式在安全相关的代码里非常普遍。
逆向小技巧:看到 CMP 后面跟着 JE,基本就是 if-else 结构。看到 CMP 后面跟着 JL/JG,多半是循环条件判断。多练习,一眼就能看出来。
9.4 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。
9.5 实战中的组合用法
单独讲每个指令都很简单,但实际代码里它们经常组合出现。我举个例子:
; 检查一个数是否是 2 的幂
mov eax, value
test eax, eax
jz not_power ; 0 不是 2 的幂
lea ecx, [eax - 1]
test eax, ecx
jnz not_power ; 如果 eax & (eax-1) != 0,则不是 2 的幂
; 到这里,eax 是 2 的幂
这段代码用了 TEST(本质上是 AND 但不写回)、LEA、JZ、JNZ。你看,一个简单的判断,就融合了逻辑运算、算术运算和条件跳转。
避坑指南:我曾经在分析一个嵌入式固件时,看到一段代码用 SHL 和 SHR 来做有符号数的除法。结果因为符号位被右移丢了,导致计算结果完全错误。记住:有符号数右移用 SAR,不是 SHR。
好了,这一章的内容就到这里。逻辑运算、移位、跳转,这三板斧是逆向分析的基本功。你可以在调试器里随便打开一个程序,单步跟踪几条指令,看看它们是怎么组合工作的。实践出真知。
公众号:蓝海资料掘金营,微信deep3321