简体   繁体   English

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

[英]Is it possible to loop infinitely with a switch statement?

I am writing a code quality tool. 我正在编写一个代码质量工具。 I am scanning source and compiled classes searching for potential infinite loops. 我正在扫描源和编译的类,搜索潜在的无限循环。

I can't think a way of a souce code switch statement can loop indefinitely. 我不能认为源代码切换语句的方式可以无限循环。 Am I wrong? 我错了吗?

Switch statements compile to lookupswitch and tableswitch opcodes. Switch语句编译为lookupswitchtableswitch操作码。 I will need to check compile classes for security reasons, and also bytecode modifications are allow before the quality control program process the compiled classes. 出于安全原因,我需要检查编译类,并且在质量控制程序处理编译的类之前还允许字节码修改。 Having say that, is there a possible way of looping infinitely by using only those opcodes by modifying a class or generating it with assembler? 话虽如此,是否有可能通过修改类或使用汇编程序生成类来仅使用那些操作码来无限循环?

I have already taken care of every other branching instructions and statements. 我已经处理了所有其他分支指令和声明。

Your help will be really appreciated. 非常感谢您的帮助。

Edit: Conclusion: 编辑:结论:

As I suspected and by the answers here provided, a switch statement in source code can only branch forward, but any branching instruction in bytecode could potentially jump backwards (assuming bytecode modifications). 正如我所怀疑的那样,通过这里提供的答案,源代码中的switch语句只能向前分支,但字节码中的任何分支指令都可能会向后跳转(假设字节码修改)。

It's interesting that you can do this with bytecode version 1.6 (50), but cannot with bytecode version 1.7 (51) as verification fails. 有趣的是,您可以使用字节码版本1.6(50)执行此操作,但不能使用字节码版本1.7(51),因为验证失败。 This code (requires ASM5) works correctly and has infinite loop: 此代码(需要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();
    }
}

However if you replace V1_6 with V1_7 , it fails with the following error: 但是,如果将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)

However if I make the forward jump instead and add a goto instruction, it works fine even with 1.7 bytecode: 但是,如果我改为向前跳转并添加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);

The difference appears due to different verification procedure: Java classes prior to Java 1.6 has no StackMapTable and verified by Type Inference , while classes with version 1.7 or higher are verified by Type Checking which has separate strict rules for individual instructions including lookupswitch . 由于不同的验证过程而出现差异:Java 1.6之前的Java类没有StackMapTable并且通过类型推断进行验证,而版本为1.7或更高版本的类通过类型检查进行验证, 类型检查对包括lookupswitch在内的各个指令具有单独的严格规则。

Currently it's unclear for me whether such instruction is actually not allowed in 1.7+ or ASM just generated incorrect StackMapTable. 目前我不清楚这种指令是否实际上不允许在1.7+或ASM中生成错误的StackMapTable。


As @Holger and @apangin noted, this is likely an ASM bug and can be worked around adding at least one case branch via mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target}); 正如@Holger和@apangin所指出的,这可能是一个ASM错误,可以通过mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target});添加至少一个案例分支mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target}); . So in conclusion: yes, you can generate backward branch in switch using any bytecode version. 总而言之:是的,您可以使用任何字节码版本在交换机中生成向后分支。

Having say that, is there a possible way of looping infinitely by using only those opcodes by modifying a class or generating it with assembler? 话虽如此,是否有可能通过修改类或使用汇编程序生成类来仅使用那些操作码来无限循环?

To have an infinite loop you have to just backward somewhere. 要拥有一个无限循环,你必须向后某个地方。 If you modify the byte code this can happen where ever you add or change a jump to go back. 如果修改字节代码,则可以在添加或更改跳转的位置执行此操作。 If not it can't be a loop, infinite or otherwise. 如果不是它不能是一个循环,无限或其他。

At the bytecode level, everything is gotos, basically. 在字节码级别,一切都是基本的。 A tableswitch or lookupswitch instruction is just a list of offsets to jump to. tableswitch或lookupswitch指令只是要跳转到的偏移列表。 You can make it jump backwards if you want. 如果你愿意,你可以让它向后跳。 You can't make it jump directly to itself, but that's only because it pops an int off the stack each time. 你无法让它直接跳转到自身,但这只是因为它每次都会从堆栈中弹出一个int。 If you prefix it with an int push, you can have a 2 instruction loop. 如果用int push作为前缀,则可以有2个指令循环。

Consider the following source code: 请考虑以下源代码:

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

When compiling it with Oracle's javac (jdk1.8), you'll get 用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

This obviously is a straight-forward translation, but this result is not mandatory. 这显然是一个直接的翻译,但这个结果不是强制性的。 The goto s at the end are actually obsolete and by compiling with Eclipse 4.4.2, I got: 最后的goto s实际上是过时的,通过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

So this compiler already omitted one of these obsolete goto s. 所以这个编译器已经省略了其中一个过时的goto But it's imaginable that another compiler would even eliminate the other goto as well, without changing the semantic: 但是可以想象另一个编译器甚至可以在不改变语义的情况下消除其他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

It's also imaginable that a byte code optimization tool is capable of taking either of the former results and transform it into the third variant. 也可以想象,字节码优化工具能够获取前一种结果并将其转换为第三种变体。 Since this is all without changing the semantic of the code, it's all still reflecting the valid Java source code shown above. 由于这一切都没有改变代码的语义,所以它仍然反映了上面显示的有效Java源代码。

So having a switch bytecode instruction producing a loop doesn't necessarily represent a logic not reproducible in Java source code. 因此,产生循环的switch字节码指令不一定代表在Java源代码中不可再现的逻辑。 It's just a compiler implementation dependent property when they never produce such a construct but more redundant code instead. 它只是一个编译器实现依赖属性,它们从不生成这样的构造,而是更多的冗余代码。 Keep always in mind that both, while / for loops and switch statements, are source code artifacts and not mandating a particular byte code form. 请记住, while / for循环和switch语句都是源代码工件,而不是强制要求特定的字节代码形式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM