I encountered a problem when using Proguard(version 5.3.3) in my android project. And I have been able to reproduce this bug stably.I simply scan the source code and I guess that it is probably a Proguard bug.
Here is the stack information:
Caused by: java.lang.IllegalArgumentException: Stack size becomes negative after instruction [18] invokestatic #29 in [xxx/pk/ProguardTest.test(Ljava/io/Closeable;Z)V] at proguard.classfile.attribute.visitor.StackSizeComputer.evaluateInstructionBlock(StackSizeComputer.java:349) at proguard.classfile.attribute.visitor.StackSizeComputer.visitExceptionInfo(StackSizeComputer.java:277) at proguard.classfile.attribute.CodeAttribute.exceptionsAccept(CodeAttribute.java:153) at proguard.classfile.attribute.visitor.StackSizeComputer.visitCodeAttribute0(StackSizeComputer.java:166) at proguard.classfile.attribute.visitor.StackSizeComputer.visitCodeAttribute(StackSizeComputer.java:125) at proguard.classfile.editor.StackSizeUpdater.visitCodeAttribute(StackSizeUpdater.java:49) at proguard.classfile.editor.MemberReferenceFixer.visitCodeAttribute(MemberReferenceFixer.java:322) at proguard.classfile.attribute.CodeAttribute.accept(CodeAttribute.java:101) at proguard.classfile.ProgramMethod.attributesAccept(ProgramMethod.java:81) at proguard.classfile.editor.MemberReferenceFixer.visitProgramMember(MemberReferenceFixer.java:285) at proguard.classfile.util.SimplifiedVisitor.visitProgramMethod(SimplifiedVisitor.java:92) at proguard.classfile.ProgramMethod.accept(ProgramMethod.java:73) at proguard.classfile.ProgramClass.methodsAccept(ProgramClass.java:516) at proguard.classfile.editor.MemberReferenceFixer.visitProgramClass(MemberReferenceFixer.java:82) at proguard.classfile.ProgramClass.accept(ProgramClass.java:358) at proguard.classfile.ClassPool.classesAccept(ClassPool.java:124) at proguard.optimize.Optimizer.execute(Optimizer.java:590) at proguard.ProGuard.optimize(ProGuard.java:328) at proguard.ProGuard.execute(ProGuard.java:127) at com.android.build.gradle.internal.transforms.BaseProguardAction.runProguard(BaseProguardAction.java:61) at com.android.build.gradle.internal.transforms.ProGuardTransform.doMinification(ProGuardTransform.java:253) ... 5 more
My testcase code:
import android.support.annotation.Keep; import android.util.Log; import java.io.Closeable; import java.io.IOException; public class ProguardTest { private static final int PRIORITY_MIN = 1; private static final String TAG = "Util"; public static int priority = 6; @Keep public static void test(Closeable closeable, boolean hideException) throws IOException { if (closeable != null) { if (hideException) { try { closeable.close(); } catch (IOException e) { e(e, "IOException"); } } else { closeable.close(); } } } private static void e(Throwable t, String message) { if (priority > PRIORITY_MIN) { e(TAG, message, t); } } private static void e(String tag, String message, Throwable t) { log(priority, tag, message + "\n" + formatThrowable(t)); } private static void log(int priority, String tag, String message) { Log.println(priority, tag, message); } private static String formatThrowable(Throwable t) { //An exception occurs when the Throwable variable is not used in the method. so we return a empty string // return t.getMessage(); return ""; } }
The following is the configuration information of Proguard:
-ignorewarnings -keepattributes Signature -keepattributes SourceFile,LineNumberTable -optimizationpasses 5 //optimizationpasses must bigger than 2 becase exception appears in the third time -optimizations !class/unboxing/enum #Prohibit method inlining,important -optimizations !method/inlining/short -optimizations !method/inlining/unique -optimizations !method/inlining/tailrecursion
Add the above code and configuration you should also get a similar exception.So I performed a breakpoint debug on Proguard and found that there is a problem with the command corresponding to the Util.close method in the codeRemovalAdvanced method(source code Optimizer.java.LineNumber 558).
befor after aload_0 v0 aload_0 v0 ifnull +26 ifnull +27 iload_1 v1 iload_1 v1 ifeq +16 ifeq +17 aload_0 v0 aload_0 v0 invokeinterface #68, 256 invokeinterface #68, 256 return return ldc #70 ldc #70 invokestatic #76 **pop2** return invokestatic #76 aload_0 v0 return invokeinterface #68, 256 aload_0 v0 return invokeinterface #68, 256 nop return return astore_1 v1 aconst_null nop return
There is a strange Pop2 instruction after optimization,So stack size becomes negative after invokestatic #76.
There is a process in Optimizer that removes the useless method parameters.when we remove a method parameter, the corresponding call location must pop out the corresponding operand in the stack.So I guess this is the reason.And i found a code in Proguard as below(source code:proguard.optimize.evaluation.EvaluationShrinker):
private class MyLocalStackConsistencyFixer extends SimplifiedVisitor implements InstructionVisitor { public void visitAnyInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, Instruction instruction) { if (isInstructionNecessary(offset)) { int popCount = instruction.stackPopCount(clazz); if (popCount > 0) { TracedStack tracedStack = partialEvaluator.getStackBefore(offset); int stackSize = tracedStack.size(); int requiredPopCount = 0; int requiredPushCount = 0; for (int stackIndex = stackSize - popCount; stackIndex < stackSize; stackIndex++) { boolean stackSimplifiedBefore = isStackSimplifiedBefore(offset, stackIndex); boolean stackEntryPresentBefore = isStackEntryPresentBefore(offset, stackIndex); if (stackSimplifiedBefore) { if (stackEntryPresentBefore) { requiredPopCount++; } } else { if (!stackEntryPresentBefore) { requiredPushCount++; } } } // Pop some unnecessary stack entries. if (requiredPopCount > 0) { **insertPopInstructions(offset, false, true, instruction, popCount);** } //other codes .... } //other codes } } .... }
insertPopInstructions funcation will insert a Pop or Pop2 instruction to code attribute,according to the above example and source code , popCount will be 2 and the Throwable parameter in method e will be optimized. so requiredPopCount will be setted as 1,but the fifth parameter of the method insertPopInstructions is passed 2(popCount),so we got the strange Pop2 instruction。
This problem has been bothering me for many days.I often suspect that it is because of my code problem (I used ASM to modify the bytecode),but I turned off my function and added the above test code. I still have this problem, so I guess it is a bug in Proguard. I hope to get your help.
Thanks for your detailed report and analysis! We can reproduce the problem. It's a bug in the EvaluationShrinker indeed. The exception argument in the invocation
e(e, "IOException")
is unused, but the code doesn't pop it properly from the stack (popping both arguments instead). We don't have a fix yet, but we're working on it.We've now fixed the bug for the upcoming ProGuard 6.1.0 (beta3).