feat(java-input): support jsr/ret opcodes (#2039)

This commit is contained in:
Skylot
2024-01-15 18:49:25 +00:00
parent 8ed48183c7
commit 306bc7abc3
19 changed files with 346 additions and 29 deletions
@@ -51,6 +51,7 @@ import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.Named;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.java.JsrNode;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
@@ -633,6 +634,17 @@ public class InsnGen {
}
break;
case JAVA_JSR:
fallbackOnlyInsn(insn);
code.add("jsr -> ").add(MethodGen.getLabelName(((JsrNode) insn).getTarget()));
break;
case JAVA_RET:
fallbackOnlyInsn(insn);
code.add("ret ");
addArg(code, insn.getArg(0));
break;
default:
throw new CodegenException(mth, "Unknown instruction: " + insn.getType());
}
@@ -104,4 +104,6 @@ public enum AFlag {
CLASS_UNLOADED, // class was completely unloaded
DONT_UNLOAD_CLASS, // don't unload class after code generation (only for tests and debug!)
RESOLVE_JAVA_JSR,
}
@@ -15,6 +15,7 @@ import jadx.api.plugins.input.insns.custom.IArrayPayload;
import jadx.api.plugins.input.insns.custom.ICustomPayload;
import jadx.api.plugins.input.insns.custom.ISwitchPayload;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.info.FieldInfo;
@@ -23,6 +24,7 @@ import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.java.JsrNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
@@ -332,6 +334,16 @@ public class InsnDecoder {
case GOTO:
return new GotoNode(insn.getTarget());
case JAVA_JSR:
method.add(AFlag.RESOLVE_JAVA_JSR);
JsrNode jsr = new JsrNode(insn.getTarget());
jsr.setResult(InsnArg.reg(insn, 0, ArgType.UNKNOWN_INT));
return jsr;
case JAVA_RET:
method.add(AFlag.RESOLVE_JAVA_JSR);
return insn(InsnType.JAVA_RET, null, InsnArg.reg(insn, 0, ArgType.UNKNOWN_INT));
case THROW:
return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE));
@@ -71,5 +71,9 @@ public enum InsnType {
PHI,
// fake insn to keep arguments which will be used in regions codegen
REGION_ARG
REGION_ARG,
// Java specific dynamic jump instructions
JAVA_JSR,
JAVA_RET,
}
@@ -69,6 +69,8 @@ public abstract class ArgType {
public static final ArgType INT_BOOLEAN = unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN);
public static final ArgType BYTE_BOOLEAN = unknown(PrimitiveType.BYTE, PrimitiveType.BOOLEAN);
public static final ArgType UNKNOWN_INT = unknown(PrimitiveType.INT);
protected int hash;
private static ArgType primitive(PrimitiveType stype) {
@@ -0,0 +1,34 @@
package jadx.core.dex.instructions.java;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.TargetInsnNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.utils.InsnUtils;
public class JsrNode extends TargetInsnNode {
protected final int target;
public JsrNode(int target) {
this(InsnType.JAVA_JSR, target, 0);
}
protected JsrNode(InsnType type, int target, int argsCount) {
super(type, argsCount);
this.target = target;
}
public int getTarget() {
return target;
}
@Override
public InsnNode copy() {
return copyCommonParams(new JsrNode(target));
}
@Override
public String toString() {
return baseString() + " -> " + InsnUtils.formatOffset(target) + attributesString();
}
}
@@ -33,6 +33,7 @@ public class FallbackModeVisitor extends AbstractVisitor {
case RETURN:
case IF:
case GOTO:
case JAVA_JSR:
case MOVE:
case MOVE_EXCEPTION:
case ARITH: // ??
@@ -60,6 +60,11 @@ public class MoveInlineVisitor extends AbstractVisitor {
}
}
SSAVar ssaVar = resultArg.getSVar();
if (ssaVar.getUseList().isEmpty()) {
// unused result
return true;
}
if (ssaVar.isUsedInPhi()) {
return false;
// TODO: review conditions of 'up' move inline (test TestMoveInline)
@@ -15,6 +15,7 @@ import jadx.core.dex.instructions.SwitchData;
import jadx.core.dex.instructions.SwitchInsn;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.java.JsrNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.blocks.BlockSplitter;
@@ -73,6 +74,14 @@ public class ProcessInstructionsVisitor extends AbstractVisitor {
addJump(mth, insnByOffset, offset, ((GotoNode) insn).getTarget());
break;
case JAVA_JSR:
addJump(mth, insnByOffset, offset, ((JsrNode) insn).getTarget());
int onRet = getNextInsnOffset(insnByOffset, offset);
if (onRet != -1) {
addJump(mth, insnByOffset, offset, onRet);
}
break;
case INVOKE:
if (insn.getResult() == null) {
ArgType retType = ((BaseInvokeNode) insn).getCallMth().getReturnType();
@@ -28,7 +28,9 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
public class BlockSplitter extends AbstractVisitor {
// leave these instructions alone in block node
/**
* Leave these instructions alone in the block node
*/
private static final Set<InsnType> SEPARATE_INSNS = EnumSet.of(
InsnType.RETURN,
InsnType.IF,
@@ -42,6 +44,18 @@ public class BlockSplitter extends AbstractVisitor {
return SEPARATE_INSNS.contains(insnType);
}
/**
* Split without connecting to the next block
*/
private static final Set<InsnType> SPLIT_WITHOUT_CONNECT = EnumSet.of(
InsnType.RETURN,
InsnType.THROW,
InsnType.GOTO,
InsnType.IF,
InsnType.SWITCH,
InsnType.JAVA_JSR,
InsnType.JAVA_RET);
@Override
public void visit(MethodNode mth) {
if (mth.isNoCode()) {
@@ -54,6 +68,10 @@ public class BlockSplitter extends AbstractVisitor {
addTempConnectionsForExcHandlers(mth, blocksMap);
expandMoveMulti(mth);
if (mth.contains(AFlag.RESOLVE_JAVA_JSR)) {
ResolveJavaJSR.process(mth);
}
removeJumpAttr(mth);
removeInsns(mth);
removeEmptyDetachedBlocks(mth);
@@ -88,27 +106,16 @@ public class BlockSplitter extends AbstractVisitor {
curBlock = connectNewBlock(mth, curBlock, insnOffset);
} else {
InsnType prevType = prevInsn.getType();
switch (prevType) {
case RETURN:
case THROW:
case GOTO:
case IF:
case SWITCH:
// split without connect to next block
curBlock = startNewBlock(mth, insnOffset);
break;
default:
if (isSeparate(prevType)
|| isSeparate(insn.getType())
|| insn.contains(AFlag.TRY_ENTER)
|| prevInsn.contains(AFlag.TRY_LEAVE)
|| insn.contains(AType.EXC_HANDLER)
|| isSplitByJump(prevInsn, insn)
|| isDoWhile(blocksMap, curBlock, insn)) {
curBlock = connectNewBlock(mth, curBlock, insnOffset);
}
break;
if (SPLIT_WITHOUT_CONNECT.contains(prevType)) {
curBlock = startNewBlock(mth, insnOffset);
} else if (isSeparate(prevType)
|| isSeparate(insn.getType())
|| insn.contains(AFlag.TRY_ENTER)
|| prevInsn.contains(AFlag.TRY_LEAVE)
|| insn.contains(AType.EXC_HANDLER)
|| isSplitByJump(prevInsn, insn)
|| isDoWhile(blocksMap, curBlock, insn)) {
curBlock = connectNewBlock(mth, curBlock, insnOffset);
}
}
blocksMap.put(insnOffset, curBlock);
@@ -201,6 +208,33 @@ public class BlockSplitter extends AbstractVisitor {
to.copyAttributesFrom(from);
}
static List<BlockNode> copyBlocksTree(MethodNode mth, List<BlockNode> blocks) {
List<BlockNode> copyBlocks = new ArrayList<>(blocks.size());
Map<BlockNode, BlockNode> map = new HashMap<>();
for (BlockNode block : blocks) {
BlockNode newBlock = startNewBlock(mth, block.getStartOffset());
copyBlockData(block, newBlock);
copyBlocks.add(newBlock);
map.put(block, newBlock);
}
for (BlockNode block : blocks) {
BlockNode newBlock = getNewBlock(block, map);
for (BlockNode successor : block.getSuccessors()) {
BlockNode newSuccessor = getNewBlock(successor, map);
BlockSplitter.connect(newBlock, newSuccessor);
}
}
return copyBlocks;
}
private static BlockNode getNewBlock(BlockNode block, Map<BlockNode, BlockNode> map) {
BlockNode newBlock = map.get(block);
if (newBlock == null) {
throw new JadxRuntimeException("Copy blocks tree failed. Missing block for connection: " + block);
}
return newBlock;
}
static void replaceTarget(BlockNode source, BlockNode oldTarget, BlockNode newTarget) {
InsnNode lastInsn = BlockUtils.getLastInsn(source);
if (lastInsn instanceof TargetInsnNode) {
@@ -0,0 +1,106 @@
package jadx.core.dex.visitors.blocks;
import java.util.ArrayList;
import java.util.List;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Duplicate code to resolve java jsr/ret.
* JSR (jump subroutine) allows executing the same code from different places.
* Used mostly for 'finally' blocks, deprecated in Java 7.
*/
public class ResolveJavaJSR {
public static void process(MethodNode mth) {
int blocksCount = mth.getBasicBlocks().size();
int k = 0;
while (true) {
boolean changed = resolve(mth);
if (!changed) {
break;
}
if (k++ > blocksCount) {
throw new JadxRuntimeException("Fail to resolve jsr instructions");
}
}
}
private static boolean resolve(MethodNode mth) {
List<BlockNode> blocks = mth.getBasicBlocks();
int blocksCount = blocks.size();
for (BlockNode block : blocks) {
if (BlockUtils.checkLastInsnType(block, InsnType.JAVA_RET)) {
resolveForRetBlock(mth, block);
if (blocksCount != mth.getBasicBlocks().size()) {
return true;
}
}
}
return false;
}
private static void resolveForRetBlock(MethodNode mth, BlockNode retBlock) {
BlockUtils.visitPredecessorsUntil(mth, retBlock, startBlock -> {
List<BlockNode> preds = startBlock.getPredecessors();
if (preds.size() > 1
&& preds.stream().allMatch(p -> BlockUtils.checkLastInsnType(p, InsnType.JAVA_JSR))) {
List<BlockNode> jsrBlocks = new ArrayList<>(preds);
List<BlockNode> dupBlocks = BlockUtils.collectAllSuccessors(mth, startBlock, false);
removeInsns(retBlock, startBlock, jsrBlocks);
processBlocks(mth, retBlock, startBlock, jsrBlocks, dupBlocks);
return true;
}
return false;
});
}
private static void removeInsns(BlockNode retBlock, BlockNode startBlock, List<BlockNode> jsrBlocks) {
InsnNode retInsn = ListUtils.removeLast(retBlock.getInstructions());
if (retInsn != null && retInsn.getType() == InsnType.JAVA_RET) {
InsnArg retArg = retInsn.getArg(0);
if (retArg.isRegister()) {
int regNum = ((RegisterArg) retArg).getRegNum();
InsnNode startInsn = BlockUtils.getFirstInsn(startBlock);
if (startInsn != null
&& startInsn.getType() == InsnType.MOVE
&& startInsn.getResult().getRegNum() == regNum) {
startBlock.getInstructions().remove(0);
}
}
}
jsrBlocks.forEach(p -> ListUtils.removeLast(p.getInstructions()));
}
private static void processBlocks(MethodNode mth, BlockNode retBlock, BlockNode startBlock,
List<BlockNode> jsrBlocks, List<BlockNode> dupBlocks) {
BlockNode first = null;
for (BlockNode jsrBlock : jsrBlocks) {
if (first == null) {
first = jsrBlock;
} else {
BlockNode pathBlock = BlockUtils.selectOther(startBlock, jsrBlock.getSuccessors());
BlockSplitter.removeConnection(jsrBlock, startBlock);
BlockSplitter.removeConnection(jsrBlock, pathBlock);
List<BlockNode> newBlocks = BlockSplitter.copyBlocksTree(mth, dupBlocks);
BlockNode newStart = newBlocks.get(dupBlocks.indexOf(startBlock));
BlockNode newRetBlock = newBlocks.get(dupBlocks.indexOf(retBlock));
BlockSplitter.connect(jsrBlock, newStart);
BlockSplitter.connect(newRetBlock, pathBlock);
}
}
if (first != null) {
BlockNode pathBlock = BlockUtils.selectOther(startBlock, first.getSuccessors());
BlockSplitter.removeConnection(first, pathBlock);
BlockSplitter.connect(retBlock, pathBlock);
}
}
}
@@ -481,19 +481,28 @@ public class BlockUtils {
}
}
public static List<BlockNode> collectAllSuccessors(MethodNode mth, BlockNode startBlock, boolean clean) {
List<BlockNode> list = new ArrayList<>(mth.getBasicBlocks().size());
dfsVisit(mth, startBlock, clean, list::add);
return list;
}
public static void dfsVisit(MethodNode mth, Consumer<BlockNode> visitor) {
dfsVisit(mth, mth.getEnterBlock(), false, visitor);
}
private static void dfsVisit(MethodNode mth, BlockNode startBlock, boolean clean, Consumer<BlockNode> visitor) {
BitSet visited = newBlocksBitSet(mth);
Deque<BlockNode> queue = new ArrayDeque<>();
BlockNode enterBlock = mth.getEnterBlock();
queue.addLast(enterBlock);
visited.set(mth.getEnterBlock().getId());
queue.addLast(startBlock);
visited.set(startBlock.getId());
while (true) {
BlockNode current = queue.pollLast();
if (current == null) {
return;
}
visitor.accept(current);
List<BlockNode> successors = current.getSuccessors();
List<BlockNode> successors = clean ? current.getCleanSuccessors() : current.getSuccessors();
int count = successors.size();
for (int i = count - 1; i >= 0; i--) { // to preserve order in queue
BlockNode next = successors.get(i);
@@ -49,6 +49,14 @@ public class ListUtils {
return list.get(list.size() - 1);
}
public static <T> @Nullable T removeLast(List<T> list) {
int size = list.size();
if (size == 0) {
return null;
}
return list.remove(size - 1);
}
public static <T extends Comparable<T>> List<T> distinctMergeSortedLists(List<T> first, List<T> second) {
if (first.isEmpty()) {
return second;
@@ -0,0 +1,23 @@
package jadx.tests.integration.others;
import org.junit.jupiter.api.Test;
import jadx.tests.api.RaungTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestJavaJSR extends RaungTest {
@Test
public void test() {
assertThat(getClassNodeFromRaung())
.code()
.containsLines(2,
"InputStream in = url.openStream();",
"try {",
indent() + "return call(in);",
"} finally {",
indent() + "in.close();",
"}");
}
}
@@ -0,0 +1,49 @@
.version 45.3
.class others/TestJavaJSR
.method public test(Ljava/net/URL;)Ljava/lang/String;
.throw java/io/IOException
.max stack 2
.max locals 6
.local 0 "this" Lothers/TestJavaJSR;
.local 1 "url" Ljava/net/URL;
.line 88
aload 1
invokevirtual java/net/URL openStream ()Ljava/io/InputStream;
astore 2
:L0
.local 2 "in" Ljava/io/InputStream;
.line 89
.line 90
aload 0
aload 2
invokevirtual others/TestJavaJSR call (Ljava/io/InputStream;)Ljava/lang/String;
astore 3
jsr :L3
aload 3
areturn
:L1
.catch all :L0 .. :L1 goto :L1
.line 89
astore 4
jsr :L3
aload 4
athrow
:L3
astore 5
.line 92
aload 2
invokevirtual java/io/InputStream close ()V
.line 89
ret 5
.end method
.method public call(Ljava/io/InputStream;)Ljava/lang/String;
.throw java/io/IOException
.max stack 1
.max locals 2
ldc ""
areturn
.end method
@@ -189,4 +189,8 @@ public enum Opcode {
CONST_METHOD_HANDLE,
CONST_METHOD_TYPE,
// Java specific dynamic jump instructions
JAVA_JSR,
JAVA_RET,
}
@@ -6,7 +6,7 @@ dependencies {
api(project(":jadx-core"))
// show bytecode disassemble
implementation("io.github.skylot:raung-disasm:0.1.0")
implementation("io.github.skylot:raung-disasm:0.1.1")
testImplementation(project(":jadx-core"))
}
@@ -253,6 +253,8 @@ public class JavaInsnsRegister {
register(arr, 0xa6, "if_acmpne", 2, 2, Opcode.IF_NE, cmp());
register(arr, 0xa7, "goto", 2, 0, Opcode.GOTO, s -> s.jump(s.s2()));
register(arr, 0xa8, "jsr", 2, 1, Opcode.JAVA_JSR, s -> s.push(0).jump(s.s2()));
register(arr, 0xa9, "ret", 1, 1, Opcode.JAVA_RET, s -> s.local(0, s.u1()));
register(arr, 0xaa, "tableswitch", -1, 1, Opcode.PACKED_SWITCH, new TableSwitchDecoder());
register(arr, 0xab, "lookupswitch", -1, 1, Opcode.SPARSE_SWITCH, new LookupSwitchDecoder());
@@ -294,6 +296,7 @@ public class JavaInsnsRegister {
register(arr, 0xc7, "ifnonnull", 2, 1, Opcode.IF_NEZ, zeroCmp());
register(arr, 0xc8, "goto_w", 4, 0, Opcode.GOTO, s -> s.jump(s.reader().readS4()));
register(arr, 0xc9, "jsr_w", 4, 1, Opcode.JAVA_JSR, s -> s.push(0).jump(s.reader().readS4()));
}
private static void dup2x1(CodeDecodeState s) {
@@ -5,5 +5,5 @@ plugins {
dependencies {
api(project(":jadx-core"))
implementation("io.github.skylot:raung-asm:0.1.0")
implementation("io.github.skylot:raung-asm:0.1.1")
}