4. P-Code操作码详解(中):INT_ADD、INT_SUB、INT_MULT、INT_DIV、INT_AND、INT_OR、INT_XOR等算术逻辑操作码

好,咱们接着聊P-Code操作码。上一章我们把数据搬移类的操作码捋了一遍,这一章进入重头戏——算术逻辑运算。说白了,就是CPU里ALU干的那摊子事。

我个人习惯把这类操作码分成两组:算术组和逻辑组。算术组管加减乘除,逻辑组管与或异或。咱们一个一个来。

4.1 算术运算操作码

4.1.1 INT_ADD — 整数加法

INT_ADD 是最常见的操作码之一。它的语义很简单:两个操作数相加,结果放到目标位置。

# P-Code 伪代码示例
# 对应 x86: add eax, ebx
INT_ADD: dest = src1 + src2

# 实际P-Code表示
(unique, 0x4) = INT_ADD (register, 0x0) (register, 0x4)

嗯,这里要注意一点:INT_ADD 不会隐式修改标志位。你可能会问:「那x86的add指令会改ZF、CF这些标志位啊,P-Code怎么处理的?」

好问题。Ghidra的做法是:把标志位修改拆成单独的P-Code操作。比如CF(进位标志)会用 INT_CARRY 操作码单独计算。这就是RTL(寄存器传输级)思想的体现——每个原子操作只做一件事。

关键点: INT_ADD 本身不产生标志位副作用。标志位由后续的 INT_CARRY、INT_SCARRY、INT_SBORROW 等操作码显式计算。

我在逆向一个加解密算法时遇到过这种情况:算法里大量使用带进位的加法,Ghidra反编译出来的P-Code序列特别长,就是因为每个加法后面跟了四五个标志位计算操作。一开始我还以为是反编译出了问题,后来才意识到这就是P-Code的设计哲学——精确、无歧义。

4.1.2 INT_SUB — 整数减法

INT_SUB 和 INT_ADD 是对称的。它执行 dest = src1 - src2。

# 对应 x86: sub eax, ecx
INT_SUB: dest = src1 - src2

# 实际P-Code表示
(unique, 0x8) = INT_SUB (register, 0x0) (register, 0x4)

这里有个坑:INT_SUB 在Ghidra内部实际上是用补码加法实现的。也就是说,Ghidra会把减法转换成「被减数 + 减数的补码」。这个细节在大多数情况下不影响分析,但如果你在做精确的数值分析,就得留个心眼。

避坑指南: 我曾经在分析一个模糊测试工具生成的样本时,发现Ghidra对某些边界情况的减法处理和我手动计算的结果对不上。排查了半天,发现是操作数大小不匹配导致的。INT_SUB 要求两个操作数的大小必须一致,否则Ghidra会做隐式扩展。这个扩展规则有时候会让人意外。

4.1.3 INT_MULT — 整数乘法

INT_MULT 执行整数乘法。和INT_ADD不同,乘法操作在P-Code里是作为一个整体出现的,不会拆成移位和加法。

# 对应 x86: imul eax, ebx
INT_MULT: dest = src1 * src2

# 实际P-Code表示
(register, 0x0) = INT_MULT (register, 0x0) (register, 0x4)

你想想看,为什么Ghidra不把乘法拆开?原因很简单:拆开后的P-Code序列会丢失「这是一个乘法操作」的语义信息。后续的优化和反编译阶段需要知道原始操作是乘法,才能做正确的类型推断和常量传播。

我记得有一次分析一个数学库,里面全是高精度乘法。Ghidra的P-Code表示非常干净,每个乘法就是一个INT_MULT,一眼就能看出计算逻辑。要是拆成移位加法,那分析起来就痛苦了。

4.1.4 INT_DIV — 整数除法

INT_DIV 处理整数除法。这里要注意:P-Code里的INT_DIV执行的是有符号除法还是无符号除法?答案是——它不区分。

等等,这听起来有点反直觉。实际上,Ghidra用两个不同的操作码来区分:INT_DIV 用于有符号除法,INT_UDIV 用于无符号除法。但很多架构的除法指令本身就分 signed 和 unsigned,所以Ghidra在翻译时会根据指令语义选择合适的操作码。

# 有符号除法
(register, 0x0) = INT_DIV (register, 0x0) (register, 0x4)

# 无符号除法
(register, 0x0) = INT_UDIV (register, 0x0) (register, 0x4)

小技巧: 在分析除法操作时,可以快速查看P-Code用的是INT_DIV还是INT_UDIV,来判断原始代码中是有符号还是无符号除法。这在分析整数溢出漏洞时特别有用。

4.2 逻辑运算操作码

4.2.1 INT_AND — 按位与

INT_AND 执行按位与操作。这个操作码在P-Code里出现频率极高,因为编译器特别喜欢用AND来做对齐、掩码、清零等操作。

# 对应 x86: and eax, 0xFF
INT_AND: dest = src1 & src2

# 实际P-Code表示
(register, 0x0) = INT_AND (register, 0x0) (const, 0xFF)

我个人习惯在分析P-Code时,看到INT_AND就条件反射地想到「这里在做位掩码」。尤其是当第二个操作数是常量时,十有八九是在提取某些位域。

4.2.2 INT_OR — 按位或

INT_OR 执行按位或操作。它通常用于设置某些位,或者合并多个值。

# 对应 x86: or eax, 0x80
INT_OR: dest = src1 | src2

# 实际P-Code表示
(register, 0x0) = INT_OR (register, 0x0) (const, 0x80)

INT_OR 和 INT_AND 经常成对出现。比如设置某个寄存器的特定位,先AND清零,再OR置位。这种模式在P-Code里一眼就能看出来。

4.2.3 INT_XOR — 按位异或

INT_XOR 执行按位异或。这个操作码有个经典用途——清零寄存器。因为 x ^ x = 0,编译器经常用XOR来代替MOV 0。

# 对应 x86: xor eax, eax
INT_XOR: dest = src1 ^ src2

# 实际P-Code表示
(register, 0x0) = INT_XOR (register, 0x0) (register, 0x0)

嗯,这里要注意:当INT_XOR的两个操作数相同时,Ghidra的优化器会把它识别为「置零」操作。但在P-Code层面,它仍然是一个完整的XOR操作。这个细节在分析混淆代码时特别重要——有些混淆器会用XOR来隐藏真正的清零意图。

4.3 操作码对比总结

为了方便查阅,我把这六个操作码整理成了一张表:

操作码 功能 典型用途 注意事项
INT_ADD 整数加法 算术运算、地址计算 不修改标志位
INT_SUB 整数减法 算术运算、比较 内部用补码实现
INT_MULT 整数乘法 算术运算、索引计算 不拆分为移位加法
INT_DIV 有符号除法 算术运算 区分有符号/无符号
INT_AND 按位与 掩码、对齐、清零 常与常量配合使用
INT_OR 按位或 置位、合并 常与AND配合使用
INT_XOR 按位异或 清零、加密、交换 相同操作数=置零

4.4 实战视角:如何利用这些操作码做分析

说了这么多理论,咱们来点实际的。我在分析一个恶意软件样本时,遇到过一段被混淆的代码。原始x86指令是:

mov eax, 0x12345678
xor eax, 0x87654321
add eax, 0x11111111

在Ghidra的P-Code视图中,这段代码被翻译成:

(register, 0x0) = COPY (const, 0x12345678)
(register, 0x0) = INT_XOR (register, 0x0) (const, 0x87654321)
(register, 0x0) = INT_ADD (register, 0x0) (const, 0x11111111)

你看,P-Code把每条指令的语义拆解得清清楚楚。即使原始指令被混淆了,P-Code层面仍然是干净的操作序列。这就是为什么我说P-Code是逆向分析的「中间语言利器」。

另外,我建议你在分析时多关注操作码的组合模式。比如:

  • INT_AND + INT_SUB:可能是取模运算的优化
  • INT_XOR + INT_ADD:可能是某种加密算法的轮函数
  • INT_MULT + INT_ADD:可能是数组索引计算

这些模式见得多了,你就能快速判断出代码的意图。

核心要点回顾:

  • 算术操作码(ADD/SUB/MULT/DIV)处理数值计算,不隐式修改标志位
  • 逻辑操作码(AND/OR/XOR)处理位运算,常用于掩码和清零
  • P-Code的原子性设计让每个操作码只做一件事,便于分析和优化
  • 操作码的组合模式能揭示代码的真实意图

好了,这一章的内容就到这里。这些算术逻辑操作码是P-Code的核心组成部分,掌握了它们,你就能看懂大部分反编译输出的中间表示。下一章我们会继续聊剩下的操作码,包括移位、比较和分支相关的。


公众号:蓝海资料掘金营,微信deep3321