寸阳分阴须爱惜,休负春色与时光。这篇文章主要讲述#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
方法内,对str
与null
进行判断,就会再次提示有潜在的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
unknown
和nullable
两者之间的区别,也很难确定这2个状态合并之后应该是一个什么样的状态。接下来,我们借助于“混沌”和“太极”的概念进行展开。在这里,我们解释一下:为什么要提到“混沌”和“太极”呢?
我们借助于“混沌”和“太极”的概念,是引入一种不同的思考方式:理解unknown、not-null、null和nullable这4个状态的区别、理解这4个种状态之间的变化关系。
1.2. Nullability
Nullness kinds有四种状态:
- unknown
- not-null
- null
- nullable
unknown
可以理解成“混沌”的状态,它代表“纯净”的物质;而not-null
和null
可以理解成由unknown
衍生出的“纯净”的“阳”和“阴”两种物质;最后,nullable
可以理解成不同比例not-null
和null
结合成的“混合”物质。文章图片
我们将任意两个状态进行合并(merge),合并的规律就是“只能向前演进,不能向后退化”。那么,任意两个状态合并之后的结果,可以使用表格来进行表示:
unknown |
not-null |
null |
nullable |
|
---|---|---|---|---|
unknown |
unknown |
not-null |
null |
nullable |
not-null |
not-null |
not-null |
nullable |
nullable |
null |
null |
nullable |
null |
nullable |
nullable |
nullable |
nullable |
nullable |
nullable |
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 <
priority2) {
return value2;
}// 第三种情况,两者不相等,但优先级相等,则一个是NOT_NULL,另一个是NULL
return NULLABLE;
}
}
举个生活当中的例子,
nullable
可以理解成不同颜色混合在一起的烟雾,加入任何其它烟雾(单纯的黄色、紫色),它仍然是混合颜色的烟雾。文章图片
那么,
unknown
、not-null
、null
和nullable
四种状态与NullPointerException
异常的关系:- 如果变量是
unknown
或not-null
状态,不会提示NullPointerException
异常。- 如果是
not-null
状态,就不可能有NullPointerException
异常。 - 如果是
unknown
状态,它有可能演进成null
的状态。如果提示NullPointerException
异常,那提示就太多了;如果提示太多了,也就没有什么用了。
- 如果是
- 如果变量是
null
或nullable
状态,就会提示NullPointerException
异常。
str.trim()
的时候,str
是unknown
的状态:public class HelloWorld {
public void test(String str) {
// State: str:nullness = unknown
System.out.println(str.trim());
}
}
相应的,在下面的代码中,加上了
if
语句对str
与null
进行判断,再次遇到str.trim()
的时候,str
是nullable
的状态: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 = https://www.songbingjia.com/android/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 =https://www.songbingjia.com/android/= 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<
BasicValue>
analyzer = new Analyzer<
>
(new NullDeferenceInterpreter(ASM9));
Frame<
BasicValue>
[] 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 <
size;
i++) {
AbstractInsnNode insn = instructions.get(i);
if (frames[i] != null) {
Value value = https://www.songbingjia.com/android/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<
BasicValue>
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<
BasicValue>
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<
MethodNode>
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
异常。借助于
NullabilityAnalyzer
和NullabilityInterpreter
类,我们来查看一下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 =https://www.songbingjia.com/android/= 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<
NullabilityValue>
implements Opcodes {
public static final Type NULL_TYPE = Type.getObjectType("null");
public static final NullabilityValue UNINITIALIZED_VALUE = https://www.songbingjia.com/android/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<
? extends NullabilityValue>
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 = https://www.songbingjia.com/android/newArrayValue(Type.getObjectType("java/lang/Object"), numDimensions);
return new NullabilityValue(arrayValue.getType(), mergedState);
}
type1 = getSuperClass(type1);
if (isAssignableFrom(type1, type2)) {
NullabilityValue arrayValue = https://www.songbingjia.com/android/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 <
dimensions;
++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<
NullabilityValue>
{
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 = https://www.songbingjia.com/android/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 <
numLocals;
i++) {
NullabilityValue currentValue = getLocal(i);
if (oldValue == currentValue) {
setLocal(i, newValue);
}
}int numStack = getMaxStackSize();
for (int i = 0;
i <
numStack;
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<
NullabilityValue>
{
public NullabilityAnalyzer(Interpreter<
NullabilityValue>
interpreter) {
super(interpreter);
}@Override
protected Frame<
NullabilityValue>
newFrame(Frame<
? extends NullabilityValue>
frame) {
return new NullabilityFrame((NullabilityFrame) frame);
}@Override
protected Frame<
NullabilityValue>
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<
NullabilityValue>
analyzer = new NullabilityAnalyzer(new NullabilityInterpreter(Opcodes.ASM9));
Frame<
NullabilityValue>
[] 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 <
size;
i++) {
AbstractInsnNode insn = instructions.get(i);
if (frames[i] != null) {
NullabilityValue value = https://www.songbingjia.com/android/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<
NullabilityValue>
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<
NullabilityValue>
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<
MethodNode>
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. 总结【#yyds干货盘点#Java ASM系列((096)检测潜在的NPE)】本文内容总结如下:
- 第一点,使用unknown、not-null、null和nullable四个状态对潜在
NullPointerException
进行分析。 - 第二点,代码示例,进行两个版本的实现。第一个版本,代码比较简单,实现的功能也比较简单;第二个版本,代码比较复杂,实现的功能也相对较强。
- 第三点,对于
NullPointerException
进行分析,仍然有很多改进的空间。在一些特殊情况下,本文呈现的思路无法进行解决。
推荐阅读
- kubeadm快速部署K8S 1.20集群
- k8s中pod删不掉
- Linux 中的 15 个强大的 firewall-cmd 命令,牛牛牛!
- 如何在Linux中使用Shell脚本终止用户会话()
- 知名科技公司员工卖了10万条客户信息,被判近四年 处罚金30万元
- #yyds干货盘点#TSM备份的设计实施手册
- Linux的文件系统
- MySQL这样写UPDATE语句,劝退
- gitlab关闭注册功能