第九章 汇编指令精讲(二):逻辑运算、移位与跳转

各位同学,咱们接着往下聊。上一章我们把算术指令过了一遍,今天要讲的这几个指令,说实话,在逆向工程里出现的频率比加减乘除还要高。为什么?因为逻辑运算和移位操作,是底层优化的核心手段,也是很多加密算法的基石。

我个人习惯把这一章的内容分成三大块:逻辑运算、移位操作、以及控制流跳转。这三块东西,你如果吃透了,看反汇编代码的速度能提升一倍。

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 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。

汇编指令精讲(二):核心知识体系 逻辑运算指令 移位指令 比较与跳转 AND OR XOR NOT SHL SHR CMP JMP Jcc 位掩码操作 | 寄存器清零 | 数据加密/解密 | 编译器优化 | 条件分支 | 循环控制 switch-case 跳转表 | 协议解析 | 反调试技巧 逻辑运算用于位操作,移位用于快速乘除,跳转控制程序流程 三者结合,构成了底层代码的基本控制单元

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