@specs-feup/alpakka
Version:
A Smali/APK source-to-source compiler written in Typescript
359 lines • 15 kB
JavaScript
import { MethodNode, Program, Statement, ReturnStatement, Label, Goto, IfComparison, IfComparisonWithZero, Switch, LabelReference, PackedSwitch, SparseSwitch, SparseSwitchElement, ThrowStatement, Catch, Instruction, MethodReference, } from "../../../../Joinpoints.js";
import UnknownInstructionNode from "./node/instruction/UnknownInstructionNode.js";
import InstructionNode from "./node/instruction/InstructionNode.js";
import StatementNode from "./node/instruction/StatementNode.js";
import ReturnNode from "./node/instruction/ReturnNode.js";
import LabelNode from "./node/instruction/LabelNode.js";
import GotoNode from "./node/instruction/GotoNode.js";
import SwitchNode from "./node/instruction/SwitchNode.js";
import ThrowNode from "./node/instruction/ThrowNode.js";
import IfComparisonNode from "./node/condition/IfComparisonNode.js";
export default class FlowGraphGenerator {
#$jp;
#graph;
#temporaryNodes;
constructor($jp, graph) {
this.#$jp = $jp;
this.#graph = graph;
this.#temporaryNodes = [];
}
build() {
if (this.#$jp instanceof Program) {
this.#$jp.classes.forEach((child) => {
child.methods.forEach((method) => {
this.#processFunction(method);
});
});
}
else if (this.#$jp instanceof MethodNode) {
this.#processFunction(this.#$jp);
}
for (const node of this.#temporaryNodes) {
node.removeFromFlow();
node.remove();
}
return this.#graph;
}
#processJp($jp, context) {
if (context.preprocessedStatementStack.length > 0) {
const [head, tail] = context.preprocessedStatementStack.pop();
return [head, tail];
}
if ($jp instanceof Label) {
return this.#processLabelStmt($jp, context);
}
else if ($jp instanceof Switch) {
return this.#processSwitch($jp, context);
}
else if ($jp instanceof IfComparison ||
$jp instanceof IfComparisonWithZero) {
return this.#processIf($jp, context);
}
else if ($jp instanceof Goto) {
return this.#processGoto($jp, context);
}
else if ($jp instanceof Catch) {
return this.#processCatchDirective($jp, context);
}
else if ($jp instanceof ReturnStatement) {
return this.#addReturnStmt(new ReturnNode.Builder($jp));
}
else if ($jp instanceof ThrowStatement) {
return this.#addThrowStmt(new ThrowNode.Builder($jp));
}
else if ($jp instanceof Statement) {
return this.#addInstruction(new StatementNode.Builder($jp));
}
else {
throw new Error(`Cannot build graph for joinpoint "${$jp.joinPointType}"`);
}
}
#processFunction($jp) {
const processedFunction = this.#graph.getFunction($jp.referenceName);
if (processedFunction !== undefined) {
return [processedFunction, undefined];
}
const context = {
labels: new Map(),
preprocessedStatementStack: new Array(),
tryCatchDirectives: new Array(),
};
const body = $jp.children.map((child) => {
const [head, tail] = this.#processJp(child, context);
return [head, tail ? [tail] : []];
});
const functionTail = [];
if (body.length === 0) {
return this.#graph.addFunction($jp, undefined, functionTail);
}
for (let i = 0; i < body.length - 1; i++) {
const [head, tail] = body[i];
const [nextHead] = body[i + 1];
if (head instanceof ReturnNode.Class || head instanceof ThrowNode.Class) {
functionTail.push(head);
}
for (const tailNode of tail) {
tailNode.nextNode = nextHead;
}
}
const [lastHead, lastTail] = body[body.length - 1];
if (lastTail.length === 0 &&
(lastHead instanceof ReturnNode.Class ||
lastHead instanceof ThrowNode.Class)) {
functionTail.push(lastHead);
}
for (const directive of context.tryCatchDirectives) {
const tryStartLabel = context.labels.get(directive.tryStart.decl.name);
if (tryStartLabel === undefined) {
throw new Error("Could not find try start label node");
}
const tryEndLabel = context.labels.get(directive.tryEnd.decl.name);
if (tryEndLabel === undefined) {
throw new Error("Could not find try end label node");
}
const catchLabel = context.labels.get(directive.catch.decl.name);
let exception = "Ljava/lang/Exception;";
if (!(directive.code.startsWith(".catchall"))) {
exception = directive.exception.code;
}
const tryEndIndex = body.findIndex(([head]) => head === tryEndLabel);
const tryStartIndex = body.findIndex(([head]) => head === tryStartLabel);
for (let i = tryEndIndex - 1; i > tryStartIndex; i--) {
const [head, tail] = body[i];
// TODO: Check if exception being thrown is caught by this catch directive
if (head instanceof ThrowNode.Class) {
if (functionTail.includes(head)) {
functionTail.splice(functionTail.indexOf(head), 1);
}
this.#connectArbitraryJump(head, catchLabel);
}
else if (head.jp instanceof Instruction &&
this.canThrow(head.jp, exception)) {
if (tail.length === 0) {
continue;
}
const nextNode = tail[0].nextNode;
if (nextNode === undefined) {
continue;
}
body[i] = [
this.#graph.addTryCatch(head.jp, nextNode, catchLabel),
[],
];
const [prevHead, previousTail] = body[i - 1];
for (const previousTailNode of previousTail) {
previousTailNode.nextNode = body[i][0];
}
if (prevHead instanceof IfComparisonNode.Class) {
// The true node in an if condition will always be a label
prevHead.falseNode = body[i][0];
}
head.removeFromFlow();
head.remove();
}
}
}
const bodyHead = body[0][0];
return this.#graph.addFunction($jp, bodyHead, functionTail);
}
canThrow($jp, $exception) {
// return $jp.canThrow;
if ($jp.opCodeName === "check-cast" &&
($exception === "Ljava/lang/ClassCastException;" ||
$exception === "Ljava/lang/RuntimeException;" ||
$exception === "Ljava/lang/Exception;")) {
return true;
}
if (($jp.opCodeName.startsWith("rem") || $jp.opCodeName.startsWith("div")) &&
($exception === "Ljava/lang/ArithmeticException;" ||
$exception === "Ljava/lang/RuntimeException;" ||
$exception === "Ljava/lang/Exception;")) {
return true;
}
// These can use dynamic dispatch, I think
// Would need further analysis to accurately determine if they can throw
if ($jp.opCodeName.startsWith("invoke-virtual") ||
$jp.opCodeName.startsWith("invoke-interface")) {
return $jp.canThrow;
}
if ($jp.opCodeName.startsWith("invoke")) {
const methodRef = $jp.children[1];
if (methodRef === undefined || !(methodRef instanceof MethodReference)) {
return false;
}
let classDescriptor = methodRef.parentClassDescriptor;
let processedFunction;
while (classDescriptor.decl !== undefined &&
processedFunction === undefined) {
classDescriptor.decl.children.forEach((child) => {
if (child instanceof MethodNode &&
child.referenceName === methodRef.code) {
// Calling this might lead to maximum call stack size exceeded, needs to be reworked
processedFunction = this.#processFunction(child)[0];
}
});
if (classDescriptor.decl.superClassDescriptor === undefined) {
break;
}
classDescriptor = classDescriptor.decl.superClassDescriptor;
}
if (processedFunction === undefined) {
// returning $jp.canThrow since we don't have access to system apis
return $jp.canThrow;
}
let instruction = undefined;
if (processedFunction.nextNodes[0] !== undefined) {
instruction = processedFunction.nextNodes[0].jp;
}
while (instruction !== undefined) {
if (instruction instanceof ThrowStatement) {
// TODO: Needs to move backwards and check exception type
return true;
}
instruction = instruction.nextStatement;
}
}
// Not sure if there are more exceptions
return false;
}
#processIf($jp, context) {
const $iftrue = $jp.label.decl;
const $iffalse = $jp.nextStatement;
if (!context.labels.has($iftrue.name)) {
this.#processLabelStmt($iftrue, context);
}
let ifTrueHead;
const label = context.labels.get($iftrue.name);
if (label !== undefined) {
ifTrueHead = label;
}
else {
const trueNode = this.#createTemporaryNode();
ifTrueHead = trueNode;
}
let ifFalseHead;
let ifFalseTail;
[ifFalseHead, ifFalseTail] = this.#processJp($iffalse, context);
context.preprocessedStatementStack.push([ifFalseHead, ifFalseTail]);
return [this.#graph.addCondition($jp, ifTrueHead, ifFalseHead)];
}
#processSwitch($jp, context) {
const $labelRef = $jp.getChild(1);
if (!($labelRef instanceof LabelReference)) {
throw new Error("Switch statement must include a label reference");
}
const $switchDecl = $labelRef.decl.nextStatement;
const defaultCase = $jp.nextStatement;
const node = this.#graph
.addNode()
.init(new SwitchNode.Builder($jp))
.as(SwitchNode.Class);
const $children = $switchDecl.children;
let previousCase = undefined;
const childrenRefs = [];
if ($switchDecl instanceof PackedSwitch) {
for (const childRef of $children) {
if (!(childRef instanceof LabelReference)) {
throw new Error("Packed switch directive children must be label references");
}
if (!context.labels.has(childRef.decl.name)) {
this.#processLabelStmt(childRef.decl, context);
}
childrenRefs.push(childRef);
}
}
else if ($switchDecl instanceof SparseSwitch) {
for (const element of $children) {
if (!(element instanceof SparseSwitchElement)) {
throw new Error("Sparse switch directive children must be sparse switch elements");
}
const childRef = element.label;
if (!(childRef instanceof LabelReference)) {
throw new Error("Sparse switch element must contain a label reference");
}
if (!context.labels.has(childRef.decl.name)) {
this.#processLabelStmt(childRef.decl, context);
}
childrenRefs.push(childRef);
}
}
for (const child of childrenRefs) {
const label = context.labels.get(child.decl.name);
if (label !== undefined) {
const currentCase = this.#graph.addSwitchCase(child, label, label);
if (previousCase === undefined) {
node.nextNode = currentCase;
}
else {
previousCase.falseNode = currentCase;
}
previousCase = currentCase;
}
}
const preProcessedStatement = this.#processJp(defaultCase, context);
context.preprocessedStatementStack.push(preProcessedStatement);
if (previousCase === undefined) {
node.nextNode = preProcessedStatement[0];
}
else {
previousCase.falseNode = preProcessedStatement[0];
}
return [node];
}
#processLabelStmt($jp, context) {
if (context.labels.has($jp.name)) {
const label = context.labels.get($jp.name);
if (label !== undefined) {
return [label, label];
}
}
const node = this.#graph
.addNode()
.init(new LabelNode.Builder($jp))
.as(LabelNode.Class);
context.labels.set($jp.name, node);
return [node, node];
}
#processGoto($jp, context) {
const node = this.#graph
.addNode()
.init(new GotoNode.Builder($jp))
.as(GotoNode.Class);
if (!context.labels.has($jp.label.decl.name)) {
this.#processLabelStmt($jp.label.decl, context);
}
const label = context.labels.get($jp.label.decl.name);
if (label !== undefined) {
this.#connectArbitraryJump(node, label);
}
return [node];
}
#processCatchDirective($jp, context) {
context.tryCatchDirectives.push($jp);
return this.#addInstruction(new StatementNode.Builder($jp));
}
#createTemporaryNode($jp) {
const node = this.#graph
.addNode()
.init(new UnknownInstructionNode.Builder($jp))
.as(UnknownInstructionNode.Class);
this.#temporaryNodes.push(node);
return node;
}
#addInstruction(builder) {
const node = this.#graph.addNode().init(builder).as(InstructionNode.Class);
return [node, node];
}
#addReturnStmt(builder) {
const node = this.#graph.addNode().init(builder).as(ReturnNode.Class);
return [node];
}
#addThrowStmt(builder) {
const node = this.#graph.addNode().init(builder).as(ThrowNode.Class);
return [node];
}
#connectArbitraryJump(from, to) {
from.nextNode = to;
}
}
//# sourceMappingURL=FlowGraphGenerator.js.map