diff options
author | zaaarf <zaaarf@proton.me> | 2023-02-07 14:20:26 +0100 |
---|---|---|
committer | zaaarf <zaaarf@proton.me> | 2023-02-07 14:20:26 +0100 |
commit | 764ff58eaa6fbfbf3e3809e67c23fcccacfa68eb (patch) | |
tree | cff32928c0b36b7b899fe44d549478007e16daf6 /src | |
parent | 809939c50264758ec4f30d58c2557f4f876f9b46 (diff) |
feat: implemented ASM pattern matcher
Diffstat (limited to 'src')
4 files changed, 310 insertions, 0 deletions
diff --git a/src/main/java/ftbsc/lll/exception/InstructionMismatchException.java b/src/main/java/ftbsc/lll/exception/InstructionMismatchException.java new file mode 100644 index 0000000..7dda1e6 --- /dev/null +++ b/src/main/java/ftbsc/lll/exception/InstructionMismatchException.java @@ -0,0 +1,19 @@ +package ftbsc.lll.exception; + +/** + * Thrown when attempting to build an InstructionSequence between two + * unconnected nodes. + */ +public class InstructionMismatchException extends RuntimeException { + public InstructionMismatchException(String message) { + super(message); + } + + public InstructionMismatchException(String message, Throwable cause) { + super(message, cause); + } + + public InstructionMismatchException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/ftbsc/lll/exception/PatternNotFoundException.java b/src/main/java/ftbsc/lll/exception/PatternNotFoundException.java new file mode 100644 index 0000000..e6d75b3 --- /dev/null +++ b/src/main/java/ftbsc/lll/exception/PatternNotFoundException.java @@ -0,0 +1,18 @@ +package ftbsc.lll.exception; + +/** + * Thrown when failing to find a pattern + */ +public class PatternNotFoundException extends RuntimeException { + public PatternNotFoundException(String message) { + super(message); + } + + public PatternNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public PatternNotFoundException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/ftbsc/lll/tools/InsnSequence.java b/src/main/java/ftbsc/lll/tools/InsnSequence.java new file mode 100644 index 0000000..fbd5f5c --- /dev/null +++ b/src/main/java/ftbsc/lll/tools/InsnSequence.java @@ -0,0 +1,31 @@ +package ftbsc.lll.tools; + +import ftbsc.lll.exception.InstructionMismatchException; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; + +import java.util.Objects; + +/** + * Represents a sequence of instructions contained within two given nodes. + * Extends InsnList, but provides additional flexibility. + */ +public class InsnSequence extends InsnList implements Opcodes { + /** + * Public constructor. + * Must be given two non-null, connected nodes. + * @param startNode the starting node of the pattern + * @param endNode the first node of the pattern + */ + public InsnSequence(AbstractInsnNode startNode, AbstractInsnNode endNode) { + Objects.requireNonNull(startNode); + Objects.requireNonNull(endNode); + for(; startNode != endNode && startNode != null; startNode = startNode.getNext()) + super.add(startNode); + if (startNode == null) + throw new InstructionMismatchException("Nodes" + getFirst() + " and " + getLast() + " are not connected."); + } + + //TODO replace +} diff --git a/src/main/java/ftbsc/lll/tools/PatternMatcher.java b/src/main/java/ftbsc/lll/tools/PatternMatcher.java new file mode 100644 index 0000000..04631fd --- /dev/null +++ b/src/main/java/ftbsc/lll/tools/PatternMatcher.java @@ -0,0 +1,242 @@ +package ftbsc.lll.tools; + +import ftbsc.lll.exception.PatternNotFoundException; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** + * Describes a pattern to match on a list of ASM instructions. + */ +public class PatternMatcher { + /** + * The list of predicates to match. + */ + private final List<Predicate<AbstractInsnNode>> predicates; + + /** + * Whether pattern search should be done from the end. + */ + private final boolean reverse; + + /** + * Patterns flagged with this ignore labels. + */ + private final boolean ignoreLabels; + + /** + * Patterns flagged with this ignore FRAME instructions. + */ + private final boolean ignoreFrames; + + /** + * Patterns flagged with this ignore LINENUMBER instructions. + */ + private final boolean ignoreLineNumbers; + + /** + * Private constructor because a PatternMatcher should only ever be initialized + * through the builder. + * @param predicates the list of predicates to match + * @param reverse search direction + * @param ignoreLabels whether LABEL instructions should be ignored + * @param ignoreFrames whether FRAME instructions should be ignored + * @param ignoreLineNumbers whether LINENUMBER instructions should be ignored + */ + private PatternMatcher(List<Predicate<AbstractInsnNode>> predicates, boolean reverse, + boolean ignoreLabels, boolean ignoreFrames, boolean ignoreLineNumbers) { + this.predicates = predicates; + this.reverse = reverse; + this.ignoreLabels = ignoreLabels; + this.ignoreFrames = ignoreFrames; + this.ignoreLineNumbers = ignoreLineNumbers; + } + + /** + * @return the Builder object for this PatternMatcher + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Tries to match the given pattern on a given MethodNode. + * @param node the MethodNode to search + * @return the InsnSequence object representing the matched pattern + */ + public InsnSequence find(MethodNode node) { + return find(reverse ? node.instructions.getLast() : node.instructions.getFirst()); + } + + /** + * Tries to match the given pattern starting from a given node. + * @param node the node to start the search on + * @return the InsnSequence object representing the matched pattern + */ + public InsnSequence find(AbstractInsnNode node) { + if(node != null) { + int match = 0; + AbstractInsnNode first = null; + AbstractInsnNode last = null; + for(AbstractInsnNode cur = node; cur != null; cur = reverse ? cur.getPrevious() : cur.getNext()) { + if(ignoreLabels && cur.getType() == AbstractInsnNode.LABEL) continue; + if(ignoreFrames && cur.getType() == AbstractInsnNode.FRAME) continue; + if(ignoreLineNumbers && cur.getType() == AbstractInsnNode.LINE) continue; + if(predicates.get(match).test(cur)) { + match++; + if(first == null) + first = cur; + } else if(match == predicates.size()) { + last = cur.getPrevious(); //it was actually the preiovus run in this case + break; + } else break; + } + if(first != null && last != null) + return new InsnSequence(first, last); + } + throw new PatternNotFoundException("Failed to find pattern!"); + } + + /** + * The Builder object for PatternMatcher. + */ + private static class Builder { + + /** + * List of predicates the pattern has to match. + */ + private final List<Predicate<AbstractInsnNode>> predicates = new ArrayList<>(); + + /** + * Whether the pattern matching should proceed in reversed order. + */ + private boolean reverse = false; + + /** + * Patterns flagged with this ignore labels. + */ + private boolean ignoreLabels = false; + + /** + * Patterns flagged with this ignore FRAME instructions. + */ + private boolean ignoreFrames = false; + + /** + * Patterns flagged with this ignore LINENUMBER instructions. + */ + private boolean ignoreLineNumbers = false; + + /** + * Builds the pattern defined so far. + * @return the built PatternMatcher + */ + public PatternMatcher build() { + return new PatternMatcher(predicates, reverse, ignoreLabels, ignoreFrames, ignoreLineNumbers); + } + + /** + * Sets the pattern to match starting from the end. + * @return the builder's state after the operation + */ + public Builder reverse() { + this.reverse = true; + return this; + } + + /** + * Adds a custom predicate to the list. Also used internally. + * @param predicate the predicate to add + * @return the builder's state after the operation + */ + public Builder check(Predicate<AbstractInsnNode> predicate) { + predicates.add(predicate); + return this; + } + + /** + * Wildcard, matches any kind of node. + * @return the builder's state after the operation + */ + public Builder any() { + return check(i -> true); + } + + /** + * Matches a specific opcode. + * @param opcode opcode to match + * @return the builder's state after the operation + */ + public Builder opcode(int opcode) { + return check(i -> i.getOpcode() == opcode); + } + + /** + * Matches a list of opcodes. + * @param opcodes list of opcodes to match + * @return the builder's state after the operation + */ + public Builder opcodes(int... opcodes) { + Builder res = this; + for(int o : opcodes) + res = opcode(o); + return res; + } + + /** + * Matches a method invokation of any kind: one of INVOKEVIRTUAL, + * INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE. + * @return the builder's state after the operation + */ + public Builder method() { + return check(i -> i.getType() == AbstractInsnNode.METHOD_INSN); + } + + /** + * Matches a field invokation of any kind: one of GETSTATIC, PUTSTATIC, + * GETFIELD or PUTFIELD. + * @return the builder's state after the operation + */ + public Builder field() { + return check(i -> i.getType() == AbstractInsnNode.FIELD_INSN); + } + + /** + * Matches any kind of jump instruction. + * @return the builder's state after the operation + */ + public Builder jump() { + return check(i -> i.getType() == AbstractInsnNode.JUMP_INSN); + } + + /** + * Tells the pattern matcher to ignore LABEL instructions. + * @return the builder's state after the operation + */ + public Builder ignoreLabels() { + this.ignoreLabels = true; + return this; + } + + /** + * Tells the pattern matcher to ignore FRAME instructions. + * @return the builder's state after the operation + */ + public Builder ignoreFrames() { + this.ignoreFrames = true; + return this; + } + + /** + * Tells the pattern matcher to ignore LINENUMBER instructions. + * @return the builder's state after the operation + */ + public Builder ignoreLineNumbers() { + this.ignoreLineNumbers = true; + return this; + } + } +} |