#java #bytecode #findbugs
#java #байт-код #findbugs
Вопрос:
Я пытаюсь написать детектор ошибок, чтобы найти экземпляры вызова метода «System.out.println» с использованием Findbugs.
Я понимаю, что «System.out.println» в байт-коде компилируется для вызова GETSTATIC, который помещает «System.out» в стек. Вызов 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));
}
}
}
}
Комментарии:
1. обязательно ли запускать его в байт-коде или его можно запустить в исходных файлах?
2. Извините, отредактировал сообщение, я забыл указать, что я использовал Findbugs (таким образом, он должен выполняться в байт-коде). Спасибо.
3. ОК. потому что PMD имеет для этого совершенно правильное правило pmd.sourceforge.net/rules/logging-java.html
4. Спасибо, однако мне интересно узнать, как детектор будет выглядеть в Findbugs, поскольку с этим «простым» примером я мог бы работать над более сложными.
Ответ №1:
Я обнаружил, что для моего варианта использования достаточно определить, что System.out или System.err используются вообще — в 99% случаев они будут использоваться для вызова .print или .println позже в блоке. Мой детектор обнаруживает коды операций GET_STATIC, которые загружают System.err или System.out. Ниже приведен код, показывающий 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()) amp;amp;
("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 amp;amp; (
// 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:
Ваша задача немного сложнее, чем кажется. Простой случай:
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()
);
Решение
Действительно, вам понадобится стек, и вам придется отслеживать каждую операцию push / pop, как определено в инструкции по байт-кодированию. Много работы для такой простой задачи…
Но если вы пойдете по этому пути, решение проблемы довольно простое: когда вы сталкиваетесь с INVOKEVIRTUAL, верхняя часть стека должна содержать некоторое значение, а значение под верхней частью должно быть « java/lang/System.out
«.
При этом я на 100% уверен, что Findbugs уже реализовал это, и, вероятно, вы можете использовать какой-нибудь FindBugs API, чтобы упростить вашу жизнь.
Комментарии:
1. Я хотел бы подтвердить, что я ранее проверял наличие такого детектора здесь: findbugs.sourceforge.net/bugDescriptions.html а также путем написания тестового примера, содержащего «System.out.println», и он не был помечен как ошибка. Я знаю, что проблема возрастает в зависимости от печатаемой строки. Я могу открыть верхнюю часть стека, используя: OpcodeStack. Элемент item = stack.getStackItem(0); однако у меня возникают некоторые трудности при сравнении его с тем, что было помещено в стек с помощью GETSTATIC. Спасибо за ваш подробный ответ.
Ответ №3:
Используйте класс OpcodeStack.
Когда вы увидите GETSTATIC и поймете, что у вас есть «out», установите пользовательское значение в сгенерированном OpcodeStack.Элемент в 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, и установлено ли значение пользователя в логическое значение.ВЕРНО, вы знаете, что находитесь в состоянии сообщить об ошибке.
Ответ №4:
Одно из решений, которое я придумал в прошлом, находит 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
вызывается.
Я видел существующие ответы, и я рассматриваю возможность изменения своего решения. Я просто добавил это для полноты картины.