简介
P-code是一种专为逆向工程而设计的寄存器传输语言。该语言通用性强,可以对不同处理器的行为进行建模,将对不同处理器的分析放入一个通用框架中,促进retargetable的分析算法和应用开发。
P-code的工作原理是将单个处理器指令转换为一组p-code操作,这些操作将处理器状态的一部分作为输入和输出变量(varnodes),这组唯一的p-code操作(和opcode区分)包含一组相当紧凑的由通用处理器执行的算术和逻辑操作。将指令直接转换为这些操作称为raw p-code。raw p-code可用于直接模拟指令执行,并且通常遵循相同的控制流,尽管它可能会添加一些自己的内部控制流。
P-code专门设计用于方便地构建数据流图,以便对反汇编后的指令进行后续分析。Varnodes和p-code操作符可以显式地视为这些图中的节点。生成raw p-code是图构造中必要的第一步,但还需要执行其他步骤,这会引入一些新的操作符。特别地其中两个新操作符MULTIEQUAL和INDIRECT用于图的构建过程,其他操作符可以在图的后续分析和转换过程中引入,并帮助保持恢复数据类型关系。最后,一些p-code操作CALL、CALLIND、RETURN可能会在分析期间更改其输入和输出varnode,导致它们不再和raw p-code形式匹配。
Ghidra的初始程序分析生成的p-code和varnode是原始(raw)的,因为仅用于表示指令的语义,很少或没有从高级语言中分析收集到的高级语义信息。Ghidra在反编译期间,p-code和varnodes被refined和关联到抽象的局部变量和源码级别的数据结构,称之为high p-code,因为它与 Ghidra中包含反编译信息的数据结构绑定,例如HighVariables和HighFunctions。
Ghidra默认情况下显示的p-code可读性较好:
然而在script中获取和处理的时候是未经翻译的原始形式,显示raw p-code的方法:Edit - Tool Options - Listing Fields - Pcode Field - Display Raw Pcode,例子:
high p-code举例(script时应该叫PcodeOpAST):
(register, 0x20, 4) CALL (ram, 0x13438, 8) , (unique, 0x10000051, 4) , (const, 0x0, 4)
(ram, 0x1d3330, 4) INDIRECT (ram, 0x1d3330, 4) , (const, 0x24, 4)
(stack, 0xffffffffffffffdc, 4) INDIRECT (stack, 0xffffffffffffffdc, 4) , (const, 0x24, 4)
(unique, 0x10000051, 4) COPY (const, 0xfa5da8, 4)
(register, 0x64, 1) INT_SLESS (register, 0x20, 4) , (const, 0x0, 4)--- CBRANCH (ram, 0x1d3318, 1) , (register, 0x64, 1)--- CALL (ram, 0x136c0, 8) , (register, 0x20, 4) , (const, 0x0, 4) , (const, 0x0, 4)
(ram, 0x1d3330, 4) INDIRECT (ram, 0x1d3330, 4) , (const, 0x3a, 4)
(stack, 0xffffffffffffffdc, 4) INDIRECT (stack, 0xffffffffffffffdc, 4) , (const, 0x3a, 4)
(unique, 0x10000059, 4) INT_ADD (register, 0x20, 4) , (const, 0xd0, 4)
(register, 0x24, 4) CAST (unique, 0x10000059, 4)
(register, 0x20, 4) CALL (ram, 0x13d54, 8) , (register, 0x20, 4) , (register, 0x24, 4) , (const, 0x400, 4)
(ram, 0x1d3330, 4) INDIRECT (ram, 0x1d3330, 4) , (const, 0x4c, 4)
(stack, 0xffffffffffffffdc, 4) INDIRECT (stack, 0xffffffffffffffdc, 4) , (const, 0x4c, 4)--- CALL (ram, 0x1216c, 8) , (register, 0x20, 4)
p-code的核心概念包括
地址空间(Address Space)
p-code的地址空间是RAM的泛化。简单的定义成可以被p-code操作读和写的可索引的字节序列。对于特定的字节,标记它的唯一索引是字节的地址。地址空间有一个名称用于识别它,一个大小表示空间中不同索引的数量,以及与之相关的endianess字节序表示整数或其他多字节的值如何编码到空间中。一个典型的处理器有一个RAM空间用于对可通过其主数据总线访问的内存进行建模,以及一个用于对处理器通用寄存器进行建模的寄存器空间(register space)。处理器操作的任何数据都必须在某个地址空间中。处理器的规范可以根据需要自由定义任意数量的地址空间。总是有一个特殊的地址空间,称为常量地址空间(const space),用于对p-code操作所需的任何常量值进行编码。生成p-code的系统通常也使用专用的临时空间(temporary space),可以将其视为临时寄存器的无尽bottomless的源,这些地址空间用于在对指令行为建模时保存中间值。
p-code的代码规范允许地址空间的可寻址单元大于一个字节。每个地址空间都有一个wordsize属性,可以设置该属性以指示一个单元中的字节数。大于1的wordsize对p-code的表示几乎没有影响。地址空间的所有偏移量仍在内部表示为字节偏移量。唯一的例外是 LOAD 和 STORE 操作,这些操作读取一个指针偏移量,当解引用指针时,该偏移量必须正确缩放以获得正确的字节偏移量。wordsize属性对任何其他 p-code操作都没有影响。
常见的地址空间包括CONST、RAM、UNIQUE、REGISTER、STACK
,其定义在ghidra/program/model/address/AddressSpace.java
Varnode
varnode是寄存器或内存位置的概括&#xff0c;由三元组表示**<地址空间、偏移量、大小>**&#xff0c;直观地说&#xff0c;varnode是某个地址空间中的连续字节序列&#xff0c;可以被视为单个值。p-code的所有操作都发生在varnode上。
Varnodes 本身只是一个连续的字节块&#xff0c;由地址和大小标识&#xff0c;没有类型。然而&#xff0c;p-code操作可以强制对 varnode 进行三种类型解释之一&#xff1a;整数、布尔值和浮点数。
- 操作整数的操作总是使用与包含 varnode 的地址空间相关的字节序将 varnode 解释为二进制补码编码。
- 用作布尔值的 varnode 被假定为单个字节&#xff0c;它只能取值 0&#xff0c;表示 false&#xff0c;1 表示 true。
- 浮点运算使用被建模的处理器所期望的编码&#xff0c;这取决于 varnode 的大小。对于大多数处理器&#xff0c;这些编码由 IEEE 754 标准描述&#xff0c;但原则上其他编码也是可能的。
如果将 varnode 指定为常量地址空间的偏移量&#xff0c;则在使用该 varnode 的任何 p 代码操作中&#xff0c;该偏移量将被解释为常量或立即值。在这种情况下&#xff0c;varnode 的大小可以被视为可用于常量编码的大小或精度。与其他 varnode 一样&#xff0c;常量只有一种类型&#xff0c;由使用它们的 p 代码操作强制。
P-code操作&#xff08;P-code Operation&#xff09;
p 码操作类似于机器指令。所有 p 码操作在内部具有相同的基本格式。它们都将一个或多个 varnode 作为输入&#xff0c;并可选择生成单个输出 varnode。操作的动作由其操作码决定。对于几乎所有 p 代码操作&#xff0c;只有输出 varnode 可以修改其值&#xff1b;操作没有间接影响。唯一可能的例外是伪操作&#xff0c;参见“伪 P-CODE 操作”一节&#xff0c;当对指令行为的了解不完整时&#xff0c;有时需要使用伪操作。
所有 p 代码操作都与它们被翻译的原始处理器指令的地址相关联。对于单个指令&#xff0c;使用从零开始的加一计数器来枚举其翻译中涉及的多个 p 码操作。地址和计数器作为一对被称为 p 码操作的唯一序列号。 p 码操作的控制流通常遵循序列号顺序。当一条指令的所有p-code执行完成时&#xff0c;如果该指令具有fall-through语义&#xff0c;则p-code控制流从fall-through地址处的指令对应的顺序的第一个p-code操作开始。类似地&#xff0c;如果 p 代码操作导致控制流分支&#xff0c;则顺序中的第一个 p 代码操作在目标地址处执行。
可能的操作码列表类似于许多基于 RISC 的指令集。每个操作码的作用在后面的章节中有详细的描述&#xff0c;在“语法参考”一节中给出了一个参考表。通常&#xff0c;特定 p 码操作的大小或精度取决于 varnode 输入或输出的大小&#xff0c;而不是操作码。
HighFunction
HighFunction是反编译器生成的函数的特定信息的集合。HighFunction由下列对象组成&#xff1a;
- 控制流表示基本块的基本功能
- 数据流表示varnodes和p-code操作
- 符号表函数访问的变量的符号表
HighSymbol
HighSymbol是反编译器恢复的显式符号之一&#xff0c;由名称和数据类型组成&#xff0c;可以描述为
Varnodes
反编译器中的Varnode和p-code操作中的varnode不是一个东西。Varnode是反编译器的核心变量概念。Varnode表示反编译器生成的函数数据流表示中的各个结点。在分析的初始阶段&#xff0c;varnode仅表示特定的存储位置&#xff0c;这些位置由各个p-code操作按顺序访问。反编译器立即将p-code转换为基于图的数据流表示&#xff0c;称为静态单赋值形式SSA。在SSA中&#xff0c;varnode具有一些附加属性。
关于SSA可以参考南大《软件分析》课程IR章节
SSA&#xff0c;每个变量都有自己的唯一定义&#xff08;def-use&#xff09;&#xff0c;有phi结点。
优点&#xff1a;唯一的变量名可以间接体现程序流信息&#xff0c;简化分析过程&#xff1b;def和use都是显式的
缺点&#xff1a;拆解出太多的变量和phi结点&#xff1b;转换回字节码存在性能问题&#xff0c;引入很多拷贝操作。
在SSA形式中&#xff0c;对存储未知的每次写入操作都会定义一个新的varnode。将代码中不同位置的操作写入同一个存储位置&#xff0c;仍然会产生不同的varnode。在这种情况下&#xff0c;每个varnode在函数内部都有一个生命周期或范围&#xff0c;开始于&#xff1a;
- 定义了变量的&#xff08;断句&#xff09;p-code操作的&#xff08;断句&#xff09;输出varnode。
- 如果varnode是函数的输入&#xff0c;则开始于函数起始位置。
varnode的范围通过控制流扩展到读取了特定的varnode作为操作数的每个p-code操作。定义p-code操作和读取操作之间varnode的值不会改变。varnode的范围可以被认为是函数体中通过控制流连接的一组地址。定义了变量的p-code操作的地址称为varnode的first use point或first use offset。
在高级语言比如C和java的反编译器输出中&#xff0c;一个varnode也具有范围&#xff0c;并且仅在代码的这个连通区域中表示高级语言中的变量。一组不相交的范围的varnode提供了对可以在函数多个位置写入的高级变量的完整描述。
HighVariable
HighVariable是一组varnodes&#xff0c;合在一起表示在反编译器输出的高级语言中整个变量的存储。每个varnode都描述了变量值在某些代码段中的存储位置。
HighVariables 和 HighSymbols 之间一般是一一对应的。 HighVariables 可以被认为是高级变量的详细存储描述&#xff0c;而 HighSymbol 提供了它的名称和数据类型。
HighVariable 总是描述函数中的指令对数据的显式操作。在某些情况下&#xff0c;HighVariable 可能只描述 HighSymbol 的部分存储。特别是对于结构化或复合数据类型&#xff0c;函数可能在代码的不同点对变量的不同部分进行操作&#xff0c;因此 HighVariable 可能只包含结构的一个字段。
一个符号可以在一个函数中被引用&#xff0c;但是这个符号的值可能没有被显式地操作。常量指针可以引用堆栈或主存储器中的变量&#xff0c;但变量的值在函数内既不读取也不写入。在这种情况下&#xff0c;HighSymbol 存在&#xff0c;但没有对应的 HighVariable。
merging
合并&#xff08;merging&#xff09;是分析过程的一部分&#xff0c;反编译器决定将哪些varnode组合在一起以在输出中创建最终的HighVariable。每个 varnode 的作用域&#xff08;参见反编译器中 Varnodes 中的讨论&#xff09;提供了对此过程的基本限制。如果两个 varnode 的作用域相交&#xff0c;则不能合并它们。但这在哪些 varnode 可以合并方面留下了很大的余地。
某些 varnode 必须合并&#xff1b;例如&#xff0c;如果它们使用相同的存储但在不同的控制流路径中&#xff0c;或者如果明确知道 varnodes 必须表示相同的变量。这称为强制合并。
反编译器还可以合并可以作为单独变量轻松存在的 varnode。这称为投机合并。除了 varnode 作用域上的交集条件外&#xff0c;反编译器仅推测性地合并共享相同数据类型的变量。除此之外&#xff0c;反编译器会优先考虑在同一指令中读取和写入的变量对&#xff0c;然后是函数控制流中彼此靠近的对。在有限的范围内&#xff0c;用户能够控制这种合并&#xff08;请参阅拆分为新变量&#xff09;。
Prototype Model
略。
参考
文档目录$ghidra_home/docs/languages/html/pcoderef.html
opcodes的子集可以出现在raw p-code中的被描述在P-Code Operation Reference(docs/languages/html/pcodedescription.html
)和Pseudo P-CODE Operations(docs/languages/html/pseudo-ops.html)构成了本文档的大部分内容。
所有新的操作码都在Additional P-CODE Operations&#xff08;docs/languages/html/additionalpcode.html
&#xff09;一节中进行了描述&#xff0c;这些操作码都不能在原始原始 p 代码转换中发生。