是否可以使用switch语句无限循环?

时间:2015-10-23 12:53:06

标签: java jvm bytecode

我正在编写一个代码质量工具。我正在扫描源代码并编译类,搜索潜在的无限循环。

我不能认为源代码切换语句的方式可以无限循环。我错了吗?

将语句编译为lookupswitchtableswitch操作码。出于安全原因,我需要检查编译类,并且在质量控制程序处理编译的类之前还允许字节码修改。 说过,通过修改类或使用汇编程序生成类,只使用那些操作码是否有可能无限循环?

我已经处理了所有其他分支指令和声明。

非常感谢您的帮助。

编辑: 结论:

正如我所怀疑的那样,通过这里提供的答案,源代码中的switch语句只能向前分支,但字节码中的任何分支指令都可能会向后跳转(假设字节码修改)。

4 个答案:

答案 0 :(得分:2)

有趣的是,您可以使用字节码版本1.6(50)执行此操作,但不能使用字节码版本1.7(51),因为验证失败。此代码(需要ASM5)正常工作并具有无限循环:

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;

public class LookupTest {
    public static void main(String[] args) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        new ClassLoader() {
            @Override
            protected Class<?> findClass(String name)
                    throws ClassNotFoundException {
                ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS
                        | ClassWriter.COMPUTE_FRAMES);
                // Create public class extending java.lang.Object
                cw.visit(V1_6, ACC_PUBLIC | ACC_SUPER, name, null,
                        "java/lang/Object", null);
                // Create default constructor
                MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V",
                        null, null);
                mv.visitCode();
                // Call superclass constructor (this is required)
                mv.visitVarInsn(ALOAD, 0);
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>",
                        "()V", false);
                // Create branch target
                Label target = new Label();
                mv.visitLabel(target);
                // System.out.println("Hello");
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
                        "Ljava/io/PrintStream;");
                mv.visitLdcInsn("Hello");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream",
                        "println", "(Ljava/lang/String;)V", false);
                // switch(0) {
                mv.visitInsn(ICONST_0);
                // default: goto target;
                // }
                mv.visitLookupSwitchInsn(target, new int[0], new Label[0]);
                mv.visitMaxs(-1, -1);
                mv.visitEnd();
                cw.visitEnd();
                byte[] bytes = cw.toByteArray();
                return defineClass(name, bytes, 0, bytes.length);
            }
        }.loadClass("LookupGotoTest").newInstance();
    }
}

但是,如果您将V1_6替换为V1_7,则会因以下错误而失败:

Exception in thread "main" java.lang.VerifyError: Bad instruction
Exception Details:
  Location:
    LookupGotoTest.<init>()V @13: lookupswitch
  Reason:
    Error exists in the bytecode
  Bytecode:
    0x0000000: 2ab7 0008 b200 0e12 10b6 0016 03ab 0000
    0x0000010: ffff fff7 0000 0000                    
  Stackmap Table:
    full_frame(@4,{Object[#2]},{})

    at java.lang.Class.getDeclaredConstructors0(Native Method)
    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2658)
    at java.lang.Class.getConstructor0(Class.java:3062)
    at java.lang.Class.newInstance(Class.java:403)
    at LookupTest.main(LookupTest.java:46)

但是如果我改为进行前向跳转并添加goto指令,即使使用1.7字节码也能正常工作:

Label target2 = new Label();
// switch(0) {
mv.visitInsn(ICONST_0);
// default: goto target2;
// }
mv.visitLookupSwitchInsn(target2, new int[0], new Label[0]);
mv.visitLabel(target2);
// goto target
mv.visitJumpInsn(GOTO, target);

由于不同的验证过程而出现差异:Java 1.6之前的Java类没有StackMapTable并且由Type Inference验证,而版本为1.7或更高的类由Type Checking验证,它具有单独的严格规则包括lookupswitch在内的个别说明。

目前我不清楚这些指令是否实际上不允许在1.7+或ASM中生成错误的StackMapTable。

正如@Holger和@apangin指出的那样,这可能是一个ASM错误,可以通过mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target});添加至少一个案例分支。总而言之:是的,您可以使用任何字节码版本在交换机中生成向后分支。

答案 1 :(得分:1)

  

如果说,通过修改类或使用汇编程序生成它,是否有可能通过仅使用那些操作码来无限循环?

要拥有一个无限循环,你必须向后某个地方。如果修改字节代码,则可以在添加或更改跳转的位置执行此操作。如果不是,它就不是一个循环,无限或其他。

答案 2 :(得分:1)

在字节码级别,一切都是基本的。 tableswitch或lookupswitch指令只是要跳转到的偏移列表。如果你愿意,你可以让它向后跳。你无法直接跳转到自身,但这只是因为它每次都会从堆栈中弹出一个int。如果用int push作为前缀,则可以有2个指令循环。

答案 3 :(得分:0)

考虑以下源代码:

public static void main(String... arg) {
    loop: for(;;) switch(arg.length) {
        case 0: continue;
        default: break loop;
    }
}

使用Oracle的javac(jdk1.8)编译时,你会得到

public static void main(java.lang.String...)
  Code:
     0: aload_0
     1: arraylength
     2: lookupswitch  { // 1
                   0: 20
             default: 23
        }
    20: goto          0
    23: goto          26
    26: return

这显然是一个直接的翻译,但这个结果不是强制性的。最后的goto实际上是过时的,通过Eclipse 4.4.2编译,我得到了:

public static void main(java.lang.String...) t
  Code:
     0: aload_0
     1: arraylength
     2: tableswitch   { // 0 to 0
                   0: 20
             default: 23
        }
    20: goto          0
    23: return

所以这个编译器已经省略了其中一个过时的goto。但是可以想象,另一个编译器甚至会在不改变语义的情况下消除其他goto

public static void main(java.lang.String...) t
  Code:
     0: aload_0
     1: arraylength
     2: tableswitch   { // 0 to 0
                   0: 0
             default: 20
        }
    20: return

也可以想象,字节码优化工具能够获取前两种结果并将其转换为第三种变体。由于这一切都没有改变代码的语义,所以它仍然反映了上面显示的有效Java源代码。

因此,生成循环的switch字节码指令不一定代表Java源代码中不可重现的逻辑。它只是一个编译器实现依赖属性,它们从不生成这样的构造,而是更多的冗余代码。请记住,while / for循环和switch语句都是源代码工件,而不是强制使用特定的字节代码形式。