热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:#yyds干货盘点#JavaASM系列:(096)检测潜在的NPE

篇首语:本文由编程笔记#小编为大家整理,主要介绍了#yyds干货盘点#JavaASM系列:(096)检测潜在的NPE相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了#yyds干货盘点#Java ASM系列:(096)检测潜在的NPE相关的知识,希望对你有一定的参考价值。




本文属于Java ASM系列三:Tree API当中的一篇。


1. 如何实现NPE分析


1.1. 代码比对

在IntelliJ IDEA当中,如果我们编写如下代码,它就会提示有NullPointerException异常:

// 第一种写法
public class HelloWorld {
public void test() {
String str = null;
// Method invocation trim will produce NullPointerException
System.out.println(str.trim());
}
}

接着,我们将str变量作为参数传递给test方法,就不会提示有潜在的NullPointerException异常:

// 第二种写法
public class HelloWorld {
public void test(String str) {
// No Warning
System.out.println(str.trim());
}
}

再进一步,在test方法内,对strnull进行判断,就会再次提示有潜在的NullPointerException异常:

// 第三种写法
public class HelloWorld {
public void test(String str) {
if (str == null) {
System.out.println("str is null");
}
else {
System.out.println("str is not null");
}
// Method invocation trim may produce NullPointerException
System.out.println(str.trim());
}
}

最后,在test方法内,将str与一个boolean flag进行关联,根据flag的值来调用str.trim()方法,不出现任何提示:

// 第四种写法
public class HelloWorld {
public void test(String str) {
boolean flag;
if (str == null) {
flag = true;
System.out.println("str is null");
}
else {
flag = false;
System.out.println("str is not null");
}
if (!flag) {
// No Warning
System.out.println(str.trim());
}
}
}

此时,我们可能会有如下的想法:



  • 第一种写法,提示NullPointerException异常,很正常,也很容易理解。

  • 第二种写法,str是有null的可能性,那么为什么不提示NullPointerException异常呢?

  • 第三种写法,比第二种写法多了if语句对null进行判断,就会再次提示有潜在的NullPointerException异常,这是为什么呢?

  • 第四种写法,添加了一个boolean类型的辅助判断,不提示NullPointerException异常,也很合理。

关于这些种写法,有各自的结果(提示或不提示NullPointerException异常),这里涉及到两组概念:



  • dereference的概念,它是一个过程,是从“变量”到“结果”的分析过程。

  • nullness相关的概念,它是一个工具,是解释从“变量”到“结果”的原因。

在nullness这个概念当中,有四个状态:unknown、not-null、null和nullable。not-null和null两个比较好理解,但是unknown和nullable的区别是什么呢?

最直观的理解方式是这样的:



  • unknown表示一种“不知道”的状态,那么“不知道”是不是意味着可能是null,也可能不是null(not-null)呢? unknown = not-null + null

  • nullable表示“可以为null”,是不是也同时意味着可以不为null(not-null)呢? nullable = not-null + null

那么,我们就很难解释清楚unknownnullable两者之间的区别,也很难确定这2个状态合并之后应该是一个什么样的状态。

接下来,我们借助于“混沌”和“太极”的概念进行展开。在这里,我们解释一下:为什么要提到“混沌”和“太极”呢?
我们借助于“混沌”和“太极”的概念,是引入一种不同的思考方式:理解unknown、not-null、null和nullable这4个状态的区别、理解这4个种状态之间的变化关系。


1.2. Nullability

Nullness kinds有四种状态:



  • unknown

  • not-null

  • null

  • nullable

其中,unknown可以理解成“混沌”的状态,它代表“纯净”的物质;而not-nullnull可以理解成由unknown衍生出的“纯净”的“阳”和“阴”两种物质;最后,nullable可以理解成不同比例not-nullnull结合成的“混合”物质。

我们将任意两个状态进行合并(merge),合并的规律就是“只能向前演进,不能向后退化”。那么,任意两个状态合并之后的结果,可以使用表格来进行表示:










































unknownnot-nullnullnullable
unknownunknownnot-nullnullnullable
not-nullnot-nullnot-nullnullablenullable
nullnullnullablenullnullable
nullablenullablenullablenullablenullable

我们以结果为导向,对上面的表格进行总结:



  • unknown:

    • unknown + unknown = unknown


  • not-null:

    • not-null + not-null = not-null

    • unknown + not-null = not-null


  • null

    • null + null = null

    • unknown + null = null


  • nullable: 剩余情况

    • nullable + nullable = nullable

    • not-null + null = nullable

    • nullable + other = nullable


其中,nullable可以理解为“最强烈的状态”,任何其它的状态都会被nullable所“掩盖”。

public enum Nullability {
UNKNOWN(0),
NOT_NULL(1),
NULL(1),
NULLABLE(2);
public final int priority;
Nullability(int priority) {
this.priority = priority;
}
public static Nullability merge(Nullability value1, Nullability value2) {
// 第一种情况,两者相等,则直接返回一个
if (value1 == value2) {
return value1;
}
// 第二种情况,两者不相等,比较优先级大小,谁大返回谁
int priority1 = value1.priority;
int priority2 = value2.priority;
if (priority1 > priority2) {
return value1;
}
else if (priority1 return value2;
}
// 第三种情况,两者不相等,但优先级相等,则一个是NOT_NULL,另一个是NULL
return NULLABLE;
}
}

举个生活当中的例子,nullable可以理解成不同颜色混合在一起的烟雾,加入任何其它烟雾(单纯的黄色、紫色),它仍然是混合颜色的烟雾。

那么,unknownnot-nullnullnullable四种状态与NullPointerException异常的关系:



  • 如果变量是unknownnot-null状态,不会提示NullPointerException异常。

    • 如果是not-null状态,就不可能有NullPointerException异常。

    • 如果是unknown状态,它有可能演进成null的状态。如果提示NullPointerException异常,那提示就太多了;如果提示太多了,也就没有什么用了。


  • 如果变量是nullnullable状态,就会提示NullPointerException异常。

在下面的代码中,遇到str.trim()的时候,strunknown的状态:

public class HelloWorld {
public void test(String str) {
// State: str:nullness = unknown
System.out.println(str.trim());
}
}

相应的,在下面的代码中,加上了if语句对strnull进行判断,再次遇到str.trim()的时候,strnullable的状态:

public class HelloWorld {
public void test(String str) {
// State: str:nullness = unknown
if (str == null) {
// State#1: str:nullness = null
System.out.println("str is null");
}
else {
// State#2: str:nullness = not-null
System.out.println("str is not null");
}
// State#1: str:nullness = null
// State#2: str:nullness = not-null
// Merged state: str:nullness = nullable
System.out.println(str.trim());
}
}

我们生活当中,经常会听到有人说“薛定谔的猫”:比喻一件事,如果你不去做,它就可能有两个结果;而一旦你去做了,最后结果就只能有一个。也就是说,你的参与直接干预了结果

接下来,就是将unknown、not-null、null和nullable这4个状态和彼此之间的转换关系应用于代码逻辑当中。


2. 代码示例:实现一

这个实现是ASM官方文档(asm4-guide.pdf)提供的实现,代码比较简单,功能也比较简单。


2.1. 预期目标

假如有一个HelloWorld类:

public class HelloWorld {
public void test(boolean flag) {
Object obj = null;
if (flag) {
obj = "10";
System.out.println(obj);
}
// Method invocation hashCode may produce NullPointerException
int hash = obj.hashCode();
System.out.println(hash);
}
}

我们的预期目标:检测出obj.hashCode()方法可能会造成NullPointerException异常。

借助于NullDeferenceInterpreter类,我们来查看一下test方法的Frame变化:

test:(Z)V
000: aconst_null {R, I, ., .} | {}
001: astore_2 {R, I, ., .} | {null}
002: iload_1 {R, I, null, .} | {}
003: ifeq L0 {R, I, null, .} | {I}
004: ldc "10" {R, I, null, .} | {}
005: astore_2 {R, I, null, .} | {R}
006: getstatic System.out {R, I, R, .} | {}
007: aload_2 {R, I, R, .} | {R}
008: invokevirtual PrintStream.println {R, I, R, .} | {R, R}
009: L0 {R, I, may-be-null, .} | {}
010: aload_2 {R, I, may-be-null, .} | {}
011: invokevirtual Object.hashCode {R, I, may-be-null, .} | {may-be-null}
012: istore_3 {R, I, may-be-null, .} | {I}
013: getstatic System.out {R, I, may-be-null, I} | {}
014: iload_3 {R, I, may-be-null, I} | {R}
015: invokevirtual PrintStream.println {R, I, may-be-null, I} | {R, I}
016: return {R, I, may-be-null, I} | {}
================================================================

011行的operand stack上,能够看到obj有可能为null值(may-be-null),这就是我们判断的依据。


2.2. 编码实现


2.2.1. NullDeferenceInterpreter

BasicInterpreter类当中,它使用BasicValue.REFERENCE_VALUE来表示所有的reference type(引用类型)。

在下面的NullDeferenceInterpreter类,继承自BasicInterpreter类。NullDeferenceInterpreter类使用三个不同的BasicValue值来表示reference type(引用类型):



  • BasicValue.REFERENCE_VALUE:表示unknown和not-null的状态。

  • NullDeferenceInterpreter.NULL_VALUE:表示null的状态。

  • NullDeferenceInterpreter.MAYBE_NULL_VALUE:表示nullable的状态。

这三个BasicValue值进行合并(merge)的操作定义在merge方法内。

import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Value;
public class NullDeferenceInterpreter extends BasicInterpreter {
public final static BasicValue NULL_VALUE = new BasicValue(NULL_TYPE);
public final static BasicValue MAYBE_NULL_VALUE = new BasicValue(Type.getObjectType("may-be-null"));
public NullDeferenceInterpreter(int api) {
super(api);
}
@Override
public BasicValue newOperation(AbstractInsnNode insn) throws AnalyzerException {
if (insn.getOpcode() == ACONST_NULL) {
return NULL_VALUE;
}
return super.newOperation(insn);
}
@Override
public BasicValue merge(BasicValue value1, BasicValue value2) {
if (isRef(value1) && isRef(value2) && value1 != value2) {
return MAYBE_NULL_VALUE;
}
return super.merge(value1, value2);
}
private boolean isRef(Value value) {
return value == BasicValue.REFERENCE_VALUE || value == NULL_VALUE || value == MAYBE_NULL_VALUE;
}
}

2.2.2. NullDereferenceDiagnosis

在下面的NullDereferenceDiagnosis类当中,主要的逻辑就是判断operand stack上的某一个元素是否可能为null值:

import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.*;
import java.util.Arrays;
import static org.objectweb.asm.Opcodes.*;
public class NullDereferenceDiagnosis {
public static int[] diagnose(String owner, MethodNode mn) throws AnalyzerException {
// 第一步,获取Frame信息
Analyzer analyzer = new Analyzer<>(new NullDeferenceInterpreter(ASM9));
Frame[] frames = analyzer.analyze(owner, mn);
// 第二步,判断是否为null或maybe-null,收集数据
TIntArrayList intArrayList = new TIntArrayList();
InsnList instructiOns= mn.instructions;
int size = instructions.size();
for (int i = 0; i AbstractInsnNode insn = instructions.get(i);
if (frames[i] != null) {
Value value = getTarget(insn, frames[i]);
if (value == NullDeferenceInterpreter.NULL_VALUE || value == NullDeferenceInterpreter.MAYBE_NULL_VALUE) {
intArrayList.add(i);
}
}
}
// 第三步,将结果转换成int[]形式
int[] array = intArrayList.toNativeArray();
Arrays.sort(array);
return array;
}
private static BasicValue getTarget(AbstractInsnNode insn, Frame frame) {
int opcode = insn.getOpcode();
switch (opcode) {
case GETFIELD:
case ARRAYLENGTH:
case MONITORENTER:
case MONITOREXIT:
return getStackValue(frame, 0);
case PUTFIELD:
return getStackValue(frame, 1);
case INVOKEVIRTUAL:
case INVOKESPECIAL:
case INVOKEINTERFACE:
String desc = ((MethodInsnNode) insn).desc;
return getStackValue(frame, Type.getArgumentTypes(desc).length);
}
return null;
}
private static BasicValue getStackValue(Frame frame, int index) {
int top = frame.getStackSize() - 1;
return index <= top ? frame.getStack(top - index) : null;
}
}

2.3. 进行分析

public class HelloWorldAnalysisTree {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes = FileUtils.readBytes(filepath);
//(1)构建ClassReader
ClassReader cr = new ClassReader(bytes);
//(2)生成ClassNode
int api = Opcodes.ASM9;
ClassNode cn = new ClassNode(api);
int parsingOptiOns= ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cn, parsingOptions);
//(3)进行分析
String className = cn.name;
List methods = cn.methods;
MethodNode mn = methods.get(1);
int[] array = NullDereferenceDiagnosis.diagnose(className, mn);
System.out.println(Arrays.toString(array));
BoxDrawingUtils.printInstructionLinks(mn.instructions, array);
}
}

运行结果:

[11]
000: aconst_null
001: astore_2
002: iload_1
003: ifeq L0
004: ldc "10"
005: astore_2
006: getstatic System.out
007: aload_2
008: invokevirtual PrintStream.println
009: L0
010: aload_2
├──── 011: invokevirtual Object.hashCode
012: istore_3
013: getstatic System.out
014: iload_3
015: invokevirtual PrintStream.println
016: return

但是,我们当前介绍的方法有局限性,无法识别下面的代码。在下面的例子当中,并没有直接将null赋值给str变量,而是判断str是否为null

public class HelloWorld {
public void test(String str) {
if (str == null) {
System.out.println("str is null");
}
else {
System.out.println("str is not null");
}
// Method invocation trim may produce NullPointerException
System.out.println(str.trim());
}
}

3. 代码示例:实现二

这个实现是项目当中提供的实现,代码比较复杂,功能也相对上一个版本较强一些。


3.1. 预期目标

public class HelloWorld {
public void test(String str) {
if (str == null) {
System.out.println("str is null");
}
else {
System.out.println("str is not null");
}
// Method invocation trim may produce NullPointerException
System.out.println(str.trim());
}
}

我们的预期目标:检测出str.trim()方法可能会造成NullPointerException异常。

借助于NullabilityAnalyzerNullabilityInterpreter类,我们来查看一下test方法的Frame变化:

test:(Ljava/lang/String;)V
000: aload_1 {HelloWorld, String} | {}
001: ifnonnull L0 {HelloWorld, String} | {String}
002: getstatic System.out {HelloWorld, String:NULL} | {}
003: ldc "str is null" {HelloWorld, String:NULL} | {PrintStream}
004: invokevirtual PrintStream.println {HelloWorld, String:NULL} | {PrintStream, String:NOT-NULL}
005: goto L1 {HelloWorld, String:NULL} | {}
006: L0 {HelloWorld, String:NOT-NULL} | {}
007: getstatic System.out {HelloWorld, String:NOT-NULL} | {}
008: ldc "str is not null" {HelloWorld, String:NOT-NULL} | {PrintStream}
009: invokevirtual PrintStream.println {HelloWorld, String:NOT-NULL} | {PrintStream, String:NOT-NULL}
010: L1 {HelloWorld, String:NULLABLE} | {}
011: getstatic System.out {HelloWorld, String:NULLABLE} | {}
012: aload_1 {HelloWorld, String:NULLABLE} | {PrintStream}
013: invokevirtual String.trim {HelloWorld, String:NULLABLE} | {PrintStream, String:NULLABLE}
014: invokevirtual PrintStream.println {HelloWorld, String:NULLABLE} | {PrintStream, String}
015: return {HelloWorld, String:NULLABLE} | {}
================================================================

013行的operand stack上,能够看到str有可能为null值(String:NULLABLE),这就是我们判断的依据。


3.2. 编码实现


3.2.1. NullabilityValue

下面的NullabilityValue类,是模拟着BasicValue类来写的,但是NullabilityValue类包含一个Nullability state字段,它记录了unknown、not-null、null和nullable四种状态。

import org.objectweb.asm.Type;
import org.objectweb.asm.tree.analysis.Value;
public class NullabilityValue implements Value {
private final Type type;
private Nullability state;
public NullabilityValue(Type type) {
this(type, Nullability.UNKNOWN);
}
public NullabilityValue(Type type, Nullability state) {
this.type = type;
this.state = state;
}
public Type getType() {
return type;
}
public void setState(Nullability state) {
this.state = state;
}
public Nullability getState() {
return state;
}
@Override
public int getSize() {
return type == Type.LONG_TYPE || type == Type.DOUBLE_TYPE ? 2 : 1;
}
public boolean isReference() {
return type != null && (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY);
}
@Override
public boolean equals(final Object value) {
if (value == this) {
return true;
}
else if (value instanceof NullabilityValue) {
NullabilityValue another = (NullabilityValue) value;
if (type == null) {
return ((NullabilityValue) value).type == null;
}
else {
return type.equals(((NullabilityValue) value).type) && state == another.state;
}
}
else {
return false;
}
}
@Override
public int hashCode() {
return type == null ? 0 : type.hashCode();
}
}

3.2.2. NullabilityInterpreter

在下面的NullabilityInterpreter类当中,它是模拟着SimpleVerifier类来写的。这个类并没有经过很多的测试,可能有许多的bug存在,所以我们就不介绍它的具体逻辑了。

import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.Interpreter;
import java.util.List;
public class NullabilityInterpreter extends Interpreter implements Opcodes {
public static final Type NULL_TYPE = Type.getObjectType("null");
public static final NullabilityValue UNINITIALIZED_VALUE = new NullabilityValue(null);
public static final NullabilityValue RETURN_ADDRESS_VALUE = new NullabilityValue(Type.VOID_TYPE);
private final ClassLoader loader = getClass().getClassLoader();
public NullabilityInterpreter(int api) {
super(api);
}
@Override
public NullabilityValue newValue(Type type) {
if (type == null) {
return UNINITIALIZED_VALUE;
}
int sort = type.getSort();
if (sort == Type.VOID) {
return null;
}
return new NullabilityValue(type);
}
@Override
public NullabilityValue newOperation(AbstractInsnNode insn) throws AnalyzerException {
switch (insn.getOpcode()) {
case ACONST_NULL:
return new NullabilityValue(NULL_TYPE, Nullability.NULL);
case ICONST_M1:
case ICONST_0:
case ICONST_1:
case ICONST_2:
case ICONST_3:
case ICONST_4:
case ICONST_5:
case BIPUSH:
case SIPUSH:
return newValue(Type.INT_TYPE);
case LCONST_0:
case LCONST_1:
return newValue(Type.LONG_TYPE);
case FCONST_0:
case FCONST_1:
case FCONST_2:
return newValue(Type.FLOAT_TYPE);
case DCONST_0:
case DCONST_1:
return newValue(Type.DOUBLE_TYPE);
case LDC:
Object value = ((LdcInsnNode) insn).cst;
if (value instanceof Integer) {
return newValue(Type.INT_TYPE);
}
else if (value instanceof Float) {
return newValue(Type.FLOAT_TYPE);
}
else if (value instanceof Long) {
return newValue(Type.LONG_TYPE);
}
else if (value instanceof Double) {
return newValue(Type.DOUBLE_TYPE);
}
else if (value instanceof String) {
return new NullabilityValue(Type.getObjectType("java/lang/String"), Nullability.NOT_NULL);
}
else if (value instanceof Type) {
int sort = ((Type) value).getSort();
if (sort == Type.OBJECT || sort == Type.ARRAY) {
return newValue(Type.getObjectType("java/lang/Class"));
}
else if (sort == Type.METHOD) {
return newValue(Type.getObjectType("java/lang/invoke/MethodType"));
}
else {
throw new AnalyzerException(insn, "Illegal LDC value " + value);
}
}
else if (value instanceof Handle) {
return newValue(Type.getObjectType("java/lang/invoke/MethodHandle"));
}
else if (value instanceof ConstantDynamic) {
return newValue(Type.getType(((ConstantDynamic) value).getDescriptor()));
}
else {
throw new AnalyzerException(insn, "Illegal LDC value " + value);
}
case JSR:
return RETURN_ADDRESS_VALUE;
case GETSTATIC:
return newValue(Type.getType(((FieldInsnNode) insn).desc));
case NEW:
return newValue(Type.getObjectType(((TypeInsnNode) insn).desc));
default:
throw new AssertionError();
}
}
@Override
public NullabilityValue copyOperation(AbstractInsnNode insn, NullabilityValue value) {
return value;
}
@Override
public NullabilityValue unaryOperation(AbstractInsnNode insn, NullabilityValue value) throws AnalyzerException {
switch (insn.getOpcode()) {
case INEG:
case IINC:
case L2I:
case F2I:
case D2I:
case I2B:
case I2C:
case I2S:
case ARRAYLENGTH:
case INSTANCEOF:
return newValue(Type.INT_TYPE);
case FNEG:
case I2F:
case L2F:
case D2F:
return newValue(Type.FLOAT_TYPE);
case LNEG:
case I2L:
case F2L:
case D2L:
return newValue(Type.LONG_TYPE);
case DNEG:
case I2D:
case L2D:
case F2D:
return newValue(Type.DOUBLE_TYPE);
case GETFIELD:
return newValue(Type.getType(((FieldInsnNode) insn).desc));
case NEWARRAY:
switch (((IntInsnNode) insn).operand) {
case T_BOOLEAN:
return newValue(Type.getType("[Z"));
case T_CHAR:
return newValue(Type.getType("[C"));
case T_BYTE:
return newValue(Type.getType("[B"));
case T_SHORT:
return newValue(Type.getType("[S"));
case T_INT:
return newValue(Type.getType("[I"));
case T_FLOAT:
return newValue(Type.getType("[F"));
case T_DOUBLE:
return newValue(Type.getType("[D"));
case T_LONG:
return newValue(Type.getType("[J"));
default:
break;
}
throw new AnalyzerException(insn, "Invalid array type");
case ANEWARRAY:
return newValue(Type.getType("[" + Type.getObjectType(((TypeInsnNode) insn).desc)));
case CHECKCAST:
return value;
case IFEQ:
case IFNE:
case IFLT:
case IFGE:
case IFGT:
case IFLE:
case TABLESWITCH:
case LOOKUPSWITCH:
case IRETURN:
case LRETURN:
case FRETURN:
case DRETURN:
case ARETURN:
case PUTSTATIC:
case ATHROW:
case MONITORENTER:
case MONITOREXIT:
case IFNULL:
case IFNONNULL:
return null;
default:
throw new AssertionError();
}
}
@Override
public NullabilityValue binaryOperation(AbstractInsnNode insn,
NullabilityValue value1,
NullabilityValue value2) {
switch (insn.getOpcode()) {
case IALOAD:
case BALOAD:
case CALOAD:
case SALOAD:
case IADD:
case ISUB:
case IMUL:
case IDIV:
case IREM:
case ISHL:
case ISHR:
case IUSHR:
case IAND:
case IOR:
case IXOR:
case LCMP:
case FCMPL:
case FCMPG:
case DCMPL:
case DCMPG:
return newValue(Type.INT_TYPE);
case FALOAD:
case FADD:
case FSUB:
case FMUL:
case FDIV:
case FREM:
return newValue(Type.FLOAT_TYPE);
case LALOAD:
case LADD:
case LSUB:
case LMUL:
case LDIV:
case LREM:
case LSHL:
case LSHR:
case LUSHR:
case LAND:
case LOR:
case LXOR:
return newValue(Type.LONG_TYPE);
case DALOAD:
case DADD:
case DSUB:
case DMUL:
case DDIV:
case DREM:
return newValue(Type.LONG_TYPE);
case AALOAD:
return getElementValue(value1);
case IF_ICMPEQ:
case IF_ICMPNE:
case IF_ICMPLT:
case IF_ICMPGE:
case IF_ICMPGT:
case IF_ICMPLE:
case IF_ACMPEQ:
case IF_ACMPNE:
case PUTFIELD:
return null;
default:
throw new AssertionError();
}
}
@Override
public NullabilityValue ternaryOperation(AbstractInsnNode insn,
NullabilityValue value1,
NullabilityValue value2,
NullabilityValue value3) {
return null;
}
@Override
public NullabilityValue naryOperation(AbstractInsnNode insn,
List values) {
int opcode = insn.getOpcode();
if (opcode == MULTIANEWARRAY) {
return newValue(Type.getType(((MultiANewArrayInsnNode) insn).desc));
}
else if (opcode == INVOKEDYNAMIC) {
return newValue(Type.getReturnType(((InvokeDynamicInsnNode) insn).desc));
}
else {
return newValue(Type.getReturnType(((MethodInsnNode) insn).desc));
}
}
@Override
public void returnOperation(AbstractInsnNode insn,
NullabilityValue value,
NullabilityValue expected) {
// Nothing to do.
}
@Override
public NullabilityValue merge(NullabilityValue value1, NullabilityValue value2) {
// 合并两者的状态
Nullability mergedState = Nullability.merge(value1.getState(), value2.getState());
// 第一种情况,两个value的类型相同且状态(state)相同
if (value1.equals(value2)) {
return value1;
}
// 第二种情况,两个value的类型相同,但状态(state)不同,需要合并它们的状态(state)
Type type1 = value1.getType();
Type type2 = value2.getType();
if (type1 != null && type1.equals(type2)) {
Type type = value1.getType();
return new NullabilityValue(type, mergedState);
}
// 第三种情况,两个value的类型不相同的,而且要合并它们的状态(state)
if (type1 != null
&& (type1.getSort() == Type.OBJECT || type1.getSort() == Type.ARRAY)
&& type2 != null
&& (type2.getSort() == Type.OBJECT || type2.getSort() == Type.ARRAY)) {
if (type1.equals(NULL_TYPE)) {
return new NullabilityValue(type2, mergedState);
}
if (type2.equals(NULL_TYPE)) {
return new NullabilityValue(type1, mergedState);
}
if (isAssignableFrom(type1, type2)) {
return new NullabilityValue(type1, mergedState);
}
if (isAssignableFrom(type2, type1)) {
return new NullabilityValue(type2, mergedState);
}
int numDimensiOns= 0;
if (type1.getSort() == Type.ARRAY
&& type2.getSort() == Type.ARRAY
&& type1.getDimensions() == type2.getDimensions()
&& type1.getElementType().getSort() == Type.OBJECT
&& type2.getElementType().getSort() == Type.OBJECT) {
numDimensiOns= type1.getDimensions();
type1 = type1.getElementType();
type2 = type2.getElementType();
}
while (true) {
if (type1 == null || isInterface(type1)) {
NullabilityValue arrayValue = newArrayValue(Type.getObjectType("java/lang/Object"), numDimensions);
return new NullabilityValue(arrayValue.getType(), mergedState);
}
type1 = getSuperClass(type1);
if (isAssignableFrom(type1, type2)) {
NullabilityValue arrayValue = newArrayValue(type1, numDimensions);
return new NullabilityValue(arrayValue.getType(), mergedState);
}
}
}
return UNINITIALIZED_VALUE;
}
protected boolean isInterface(final Type type) {
return getClass(type).isInterface();
}
protected Type getSuperClass(final Type type) {
Class superClass = getClass(type).getSuperclass();
return superClass == null ? null : Type.getType(superClass);
}
private NullabilityValue newArrayValue(final Type type, final int dimensions) {
if (dimensiOns== 0) {
return newValue(type);
}
else {
StringBuilder descriptor = new StringBuilder();
for (int i = 0; i descriptor.append([);
}
descriptor.append(type.getDescriptor());
return newValue(Type.getType(descriptor.toString()));
}
}
protected NullabilityValue getElementValue(final NullabilityValue objectArrayValue) {
Type arrayType = objectArrayValue.getType();
if (arrayType != null) {
if (arrayType.getSort() == Type.ARRAY) {
return newValue(Type.getType(arrayType.getDescriptor().substring(1)));
}
else if (arrayType.equals(NULL_TYPE)) {
return objectArrayValue;
}
}
throw new AssertionError();
}
protected boolean isSubTypeOf(final NullabilityValue value, final NullabilityValue expected) {
Type expectedType = expected.getType();
Type type = value.getType();
switch (expectedType.getSort()) {
case Type.INT:
case Type.FLOAT:
case Type.LONG:
case Type.DOUBLE:
return type.equals(expectedType);
case Type.ARRAY:
case Type.OBJECT:
if (type.equals(NULL_TYPE)) {
return true;
}
else if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
if (isAssignableFrom(expectedType, type)) {
return true;
}
else if (getClass(expectedType).isInterface()) {
// The merge of class or interface types can only yield class types (because it is not
// possible in general to find an unambiguous common super interface, due to multiple
// inheritance). Because of this limitation, we need to relax the subtyping check here
// if value is an interface.
return Object.class.isAssignableFrom(getClass(type));
}
else {
return false;
}
}
else {
return false;
}
default:
throw new AssertionError();
}
}
protected boolean isAssignableFrom(final Type type1, final Type type2) {
if (type1.equals(type2)) {
return true;
}
return getClass(type1).isAssignableFrom(getClass(type2));
}
protected Class getClass(final Type type) {
try {
if (type.getSort() == Type.ARRAY) {
return Class.forName(type.getDescriptor().replace(/, .), false, loader);
}
return Class.forName(type.getClassName(), false, loader);
}
catch (ClassNotFoundException e) {
throw new TypeNotPresentException(e.toString(), e);
}
}
}

3.2.3. NullabilityFrame

在下面的NullabilityFrame当中,重点是关注initJumpTarget方法。Overriding this method and changing the frame values allows implementing branch-sensitive analyses.

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.analysis.Frame;
public class NullabilityFrame extends Frame {
public NullabilityFrame(int numLocals, int numStack) {
super(numLocals, numStack);
}
public NullabilityFrame(NullabilityFrame frame) {
super(frame);
}
@Override
public void initJumpTarget(int opcode, LabelNode target) {
// 首先,处理自己的代码逻辑
int stackIndex = getStackSize();
NullabilityValue oldValue = getStack(stackIndex);
switch (opcode) {
case Opcodes.IFNULL: {
if (target == null) {
updateFrame(oldValue, Nullability.NOT_NULL);
}
else {
updateFrame(oldValue, Nullability.NULL);
}
break;
}
case Opcodes.IFNONNULL: {
if (target == null) {
updateFrame(oldValue, Nullability.NULL);
}
else {
updateFrame(oldValue, Nullability.NOT_NULL);
}
break;
}
}
// 其次,调用父类的方法实现
super.initJumpTarget(opcode, target);
}
private void updateFrame(NullabilityValue oldValue, Nullability newState) {
NullabilityValue newValue = new NullabilityValue(oldValue.getType(), newState);
int numLocals = getLocals();
for (int i = 0; i NullabilityValue currentValue = getLocal(i);
if (oldValue == currentValue) {
setLocal(i, newValue);
}
}
int numStack = getMaxStackSize();
for (int i = 0; i NullabilityValue currentValue = getStack(i);
if (oldValue == currentValue) {
setStack(i, newValue);
}
}
}
}

3.2.4. NullabilityAnalyzer

在下面的NullabilityAnalyzer当中,重点是关注newFrame方法,它使用NullabilityFrame替换掉了Frame类的实现:

import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.Interpreter;
public class NullabilityAnalyzer extends Analyzer {
public NullabilityAnalyzer(Interpreter interpreter) {
super(interpreter);
}
@Override
protected Frame newFrame(Frame frame) {
return new NullabilityFrame((NullabilityFrame) frame);
}
@Override
protected Frame newFrame(int numLocals, int numStack) {
return new NullabilityFrame(numLocals, numStack);
}
}

3.2.5. NullabilityDiagnosis

NullabilityDiagnosis类当中,主要的逻辑也是判断operand stack上的某一个元素是否可能为null值:

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.Frame;
import java.util.Arrays;
import static org.objectweb.asm.Opcodes.*;
public class NullabilityDiagnosis {
public static int[] diagnose(String className, MethodNode mn) throws AnalyzerException {
// 第一步,获取Frame信息
Analyzer analyzer = new NullabilityAnalyzer(new NullabilityInterpreter(Opcodes.ASM9));
Frame[] frames = analyzer.analyze(className, mn);
// 第二步,判断是否为null或maybe-null,收集数据
TIntArrayList intArrayList = new TIntArrayList();
InsnList instructiOns= mn.instructions;
int size = instructions.size();
for (int i = 0; i AbstractInsnNode insn = instructions.get(i);
if (frames[i] != null) {
NullabilityValue value = getTarget(insn, frames[i]);
if (value == null) continue;
if (value.getState() == Nullability.NULL || value.getState() == Nullability.NULLABLE) {
intArrayList.add(i);
}
}
}
// 第三步,将结果转换成int[]形式
int[] array = intArrayList.toNativeArray();
Arrays.sort(array);
return array;
}
private static NullabilityValue getTarget(AbstractInsnNode insn, Frame frame) {
int opcode = insn.getOpcode();
switch (opcode) {
case GETFIELD:
case ARRAYLENGTH:
case MONITORENTER:
case MONITOREXIT:
return getStackValue(frame, 0);
case PUTFIELD:
return getStackValue(frame, 1);
case INVOKEVIRTUAL:
case INVOKESPECIAL:
case INVOKEINTERFACE:
String desc = ((MethodInsnNode) insn).desc;
return getStackValue(frame, Type.getArgumentTypes(desc).length);
}
return null;
}
private static NullabilityValue getStackValue(Frame frame, int index) {
int top = frame.getStackSize() - 1;
return index <= top ? frame.getStack(top - index) : null;
}
}

3.3. 进行分析

public class HelloWorldAnalysisTree {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes = FileUtils.readBytes(filepath);
//(1)构建ClassReader
ClassReader cr = new ClassReader(bytes);
//(2)生成ClassNode
int api = Opcodes.ASM9;
ClassNode cn = new ClassNode(api);
int parsingOptiOns= ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cn, parsingOptions);
//(3)进行分析
String className = cn.name;
List methods = cn.methods;
MethodNode mn = methods.get(1);
int[] array = NullabilityDiagnosis.diagnose(className, mn);
System.out.println(Arrays.toString(array));
BoxDrawingUtils.printInstructionLinks(mn.instructions, array);
}
}

输出结果:

[13]
000: aload_1
001: ifnonnull L0
002: getstatic System.out
003: ldc "str is null"
004: invokevirtual PrintStream.println
005: goto L1
006: L0
007: getstatic System.out
008: ldc "str is not null"
009: invokevirtual PrintStream.println
010: L1
011: getstatic System.out
012: aload_1
├──── 013: invokevirtual String.trim
014: invokevirtual PrintStream.println
015: return

4. 测试用例


4.1. unknown

public class HelloWorld {
public void test(String str) {
System.out.println(str.trim());
}
}

4.2. not-null

public class HelloWorld {
public void test() {
String str = "ABC";
System.out.println(str.trim());
}
}

4.3. null

public class HelloWorld {
public void test() {
String str = null;
System.out.println(str.trim());
}
}

4.4. if: not-null+null=nullable

public class HelloWorld {
public void test(String str) {
// unknown
if (str == null) {
// null
System.out.println("str is null");
}
else {
// not-null
System.out.println("str is not null");
}
// nullable
System.out.println(str.trim());
}
}

4.5. Infeasible Paths

public class HelloWorld {
public void test(String str) {
boolean flag;
if (str != null) {
flag = true;
}
else {
flag = false;
}
if (flag) {
// flag记录了str的状态,因此不会出现NullPointerException异常
int length = str.length();
System.out.println(length);
}
}
}

5. 总结

本文内容总结如下:



  • 第一点,使用unknown、not-null、null和nullable四个状态对潜在NullPointerException进行分析。

  • 第二点,代码示例,进行两个版本的实现。第一个版本,代码比较简单,实现的功能也比较简单;第二个版本,代码比较复杂,实现的功能也相对较强。

  • 第三点,对于NullPointerException进行分析,仍然有很多改进的空间。在一些特殊情况下,本文呈现的思路无法进行解决。


推荐阅读
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • GreenDAO快速入门
    前言之前在自己做项目的时候,用到了GreenDAO数据库,其实对于数据库辅助工具库从OrmLite,到litePal再到GreenDAO,总是在不停的切换,但是没有真正去了解他们的 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 本文介绍了UVALive6575题目Odd and Even Zeroes的解法,使用了数位dp和找规律的方法。阶乘的定义和性质被介绍,并给出了一些例子。其中,部分阶乘的尾零个数为奇数,部分为偶数。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
author-avatar
hytyj_989
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有