编写检测器以使用Findbugs搜索“System.out.println”的用法

时间:2011-10-09 14:37:53

标签: java bytecode findbugs

我正在尝试使用Findbugs编写一个bug检测器来查找方法调用“System.out.println”的实例。

我理解字节码中的“System.out.println”被编译为对GETSTATIC which pushes "System.out" onto the stack的调用。对INVOKEVIRTUAL的调用会从堆栈中弹出“System.out”并调用该方法。

我准备了一些代码(如下所示),它找到了正确的GETSTATIC和INVOKEVIRTUAL调用,但无法将两者联系在一起。我怀疑我可能需要以某种方式使用OpcodeStack,但我很难理解如何使用它。任何帮助,将不胜感激。

    @Override 
    public void sawOpcode(int seen) { 
            // if opcode is getstatic 
            if (seen == GETSTATIC) { 
                    String clsName = getClassConstantOperand(); 
                    if ("java/lang/System".equals(clsName)) { 
                            String fldName = getNameConstantOperand(); 
                            if ("out".equals(fldName)) { 
                                    System.out.println("SYSTEM.OUT here"); 
                            } 
                    } 
            } 

            // if opcode is invokevirtual 
            if (seen == INVOKEVIRTUAL) { 
                    String cls = getDottedClassConstantOperand(); 
                    if ("java.io.PrintStream".equals(cls)) { 
                            String methodName = getNameConstantOperand(); 
                            if ("println".equals(methodName)) { 
                                    bugReporter.reportBug(new BugInstance("SYSTEM_OUT_PRINTLN", 
                                                    NORMAL_PRIORITY).addClassAndMethod(this) 
                                                    .addSourceLine(this)); 
                            } 
                    } 
            } 

    }

4 个答案:

答案 0 :(得分:1)

你的任务比看起来要复杂一些。一个简单的案例:

System.out.println("abc");

也被翻译成一个简单的字节码:

getstatic   #2; //java/lang/System.out
ldc #3; //String abc
invokevirtual   #4; //Calling java/io/PrintStream.println(String)

但是,如果您尝试打印除简单常量/已知值以外的任何内容,则会变得更难:

int x = 42;
System.out.println(x + 17);

将被翻译为:

bipush  42
istore_1  //x = 42
getstatic   #2; //java/lang/System.out
iload_1  //x
bipush  17
iadd  //x + 17 on the stack
invokevirtual   #5; //Calling java/io/PrintStream.println(int)

但等等,情况会变得更糟:

System.out.println("x times 27 is " + x * 27);

什么? StringBuilder:?

new #6; //new java/lang/StringBuilder()
dup
invokespecial   #7; //Calling java/lang/StringBuilder()
ldc #8; //String x times 2 is
invokevirtual   #9; //Calling java/lang/StringBuilder.append(String)
iload_1  //x
bipush  27
imul  //x * 27 on the stack
invokevirtual   #10; //Calling java/lang/StringBuilder.append:(int) with 'x * 27' argument
invokevirtual   #11; //Calling java/lang/StringBuilder.toString:()
invokevirtual   #4; //Calling java/io/PrintStream.println(String)

有趣的是,原始代码被翻译为(这是一个已知的Java 5(?)优化):

System.out.println(
  new StringBuilder().
    append("x times 27 is ").
    append(x * 27).
    toString()
  );

解决方案

确实 - 你需要一个堆栈,你必须跟踪bytecode instruction中定义的每个推/弹操作。为这么简单的任务做了很多工作......

但是如果你走这条路,解决问题很简单:遇到INVOKEVIRTUAL时,堆栈顶部应包含一些值,顶部下方的值应为“java/lang/System.out”。

话虽如此,我100%肯定Findbugs已经实现了这一点,可能你可以使用一些FindBugs API来让你的生活更轻松。

答案 1 :(得分:1)

我发现,对于我的用例,足以确定所有使用System.out或System.err - 在99%的情况下,这些将用于调用。稍后在块中打印或.println。我的检测器检测到加载System.err或System.out的GET_STATIC操作码。代码如下,显示了确定发生这种情况的3种选择。

package my.findbugs.detectors.forbiddencalls;

import org.apache.log4j.Logger; // it is not trivial to use a logger with FindBugs in Eclipse, leave it out if there are problems

import my.findbugs.detectors.util.DetectorUtil;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;


public class CallToSystemOutPrintlnDetector2 extends OpcodeStackDetector {

    private static final Logger LOGGER = Logger.getLogger(CallToSystemOutPrintlnDetector2.class);
    private BugReporter bugReporter;


    public CallToSystemOutPrintlnDetector2(BugReporter bugReporter) {
        super();
        this.bugReporter = bugReporter;
        LOGGER.debug("Instantiated.");
    }


    public void sawOpcode(int seen) {

        // find occurrences of:  
        //2:   getstatic       #54; //Field java/lang/System.out:Ljava/io/PrintStream;
//2:   getstatic       #54; //Field java/lang/System.out:Ljava/io/PrintStream;

        if (seen == GETSTATIC){

            try {
//              LOGGER.debug(operand); // static java.lang.System.out Ljava/io/PrintStream;
//              LOGGER.debug(operand.getClass()); // class edu.umd.cs.findbugs.classfile.analysis.FieldInfo
//              LOGGER.debug(operand.getName()); // err
//              LOGGER.debug(operand.getClassDescriptor()); // java/lang/System
//              LOGGER.debug(operand.getSignature()); // Ljava/io/PrintStream;

                FieldDescriptor operand = getFieldDescriptorOperand();
                ClassDescriptor classDescriptor = operand.getClassDescriptor();
                if ("java/lang/System".equals(classDescriptor.getClassName()) && 
                        ("err".equals(operand.getName())||"out".equals(operand.getName()))) {
                    reportBug();
                }
            } catch (Exception e) {
                //ignore
            }

            // could be used
//          try {
//              MethodDescriptor operand = getMethodDescriptorOperand();
//              LOGGER.debug(operand); // java.lang.System.outLjava/io/PrintStream;
//              LOGGER.debug(operand.getClass()); // class edu.umd.cs.findbugs.classfile.MethodDescriptor
//              LOGGER.debug(operand.getName()); // err 
//              LOGGER.debug(operand.getClassDescriptor()); // java/lang/System
//              LOGGER.debug(operand.getSignature()); // Ljava/io/PrintStream;
//          } catch (Exception e) {
//              //ignore
//          }

            // could be used
//          try {
//              String operand = getRefConstantOperand();
//              LOGGER.debug(operand); // java.lang.System.out : Ljava.io.PrintStream;
//              if (operand != null && (
//                  operand.startsWith("java.lang.System.out :") || operand.startsWith("java.lang.System.err :"))) {
//                  reportBug();
//              }
//          } catch (Exception e) {
//              //ignore
//          }
        }
    }

    private void reportBug(){
        this.bugReporter.reportBug(getBugInstance());
    }


    private BugInstance getBugInstance() {
        return new BugInstance(this, "MY_CALL_TO_SYSTEM_OUT_BUG", DetectorUtil.MY_PRIORITY)
            .addClassAndMethod(this)
            .addSourceLine(this);
    }

}

答案 2 :(得分:0)

使用OpcodeStack类。

当你看到GETSTATIC,并且你意识到你已经'out'时,将生成的OpcodeStack.Item上的用户值设置为Boolean.TRUE。要做到这一点

try {
     //process opcodes
} finally {
    super.sawOpcode(seen);
    if (pseudocode-saw System.out.println) {
        OpcodeStack.Item item = stack.getStackItem(0);
        item.setUserValue(Boolean.TRUE);
}

然后在处理println调用时,查看tos,如果用户值设置为Boolean.TRUE,则表示您处于报告错误的状态。

答案 3 :(得分:0)

我在过去提出的一个解决方案是System.out.println调用“括号内没有太多计算”,即最多有MAX_WILDCARDS指令。

我扩展了ByteCodePatternDetector,我的模式如下:

ByteCodePattern pattern = new ByteCodePattern();
// as this is a pattern, I cannot specify here that this is supposed to be System.out
pattern.add(new Load(SYSO_FIELD, "sysoResult")); 
pattern.add(new Wild(MAX_WILDCARDS)); 
pattern.add(new Invoke("java.io.PrintStream", "println", "/.*", Invoke.INSTANCE, null).label(LABEL_PRINT));

我稍后确保加载和调用操作的字节代码是正确的,并比较我从match.getBindingSet().lookup(...)检索的类和字段名称,以确保它被System.out调用。

我已经看到了现有的答案,我考虑改变我的解决方案。我只是为了完整起见而添加了这个。