Unsage类的使用

付威     2019-07-23   6463   18min  

Java是以安全著称,但在Java中有一个类是一个Bug级别的存在,那就是Unsafe. 前面已经说过Unsafe在java中的使用,此处我们直接说用法:

避免初始化

当你想跳过对象初始化的阶段,或者绕过构造函数的检查,去实例化没有任何公共构造函数的类,可以使用allocateInstance:


    class A {
        private long a; // not initialized value

        public A() {
            this.a = 1; // initialization
        }

        public long a() { return this.a; }
    }

使用构造函数、反射和unsafe初始化它,将得到不同的结果。

    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        A o1 = new A(); // constructor
        System.out.println(o1.a()); // prints 1

        A o2 = A.class.newInstance(); // reflection
        System.out.println(o2.a()); // prints 1

        A o3 = (A) UnsafeUtils.getUnsafe().allocateInstance(A.class); // unsafe
        System.out.println(o3.a()); // prints 0
    }

内存崩溃(Memory corruption)

我们可以使用Unsafe去做一些绕过安全的技术:

    class Guard {
        private int ACCESS_ALLOWED = 1;

        public boolean giveAccess() {
                return 42 == ACCESS_ALLOWED;
        }
    }

当客户端调用giveAccess代码是,始终返回的都是false.使用Unsafe可以绕过权限:

    Guard guard=new Guard();
    System.out.println("修改前:"+guard.giveAccess());
    Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
    unsafe.putInt(guard,  unsafe.objectFieldOffset(f), 42); // memory corruption
    System.out.println("修改后:"+guard.giveAccess());

计算对象的大小

sizeOf是返回对象的自身内存大小。

 public static long sizeOf(Object o) {
        Unsafe u = UnsafeUtils.getUnsafe();
        HashSet<Field> fields = new HashSet<Field>();
        Class c = o.getClass();
        while (c != Object.class) {
            for (Field f : c.getDeclaredFields()) {
                if ((f.getModifiers() & Modifier.STATIC) == 0) {
                    fields.add(f);
                }
            }
            c = c.getSuperclass();
        }
        // get offset
        long maxSize = 0;
        for (Field f : fields) {
            long offset = u.objectFieldOffset(f);
            if (offset > maxSize) {
                maxSize = offset;
            }
        }

        return ((maxSize/8) + 1) * 8;   // padding
    }

如果只是对象类的结构大小,那么可以更简单的实现:

  public static long sizeOf(Object object){
        return UnsafeUtils.getUnsafe().getAddress(
                normalize(UnsafeUtils.getUnsafe().getInt(object, 4L)) + 12L);
    }
    private static long normalize(int value) {
        if(value >= 0) return value;
        return (~0L >>> 32) & value;
    }

浅拷贝(Shallow copy)

为了计算自身内存的大小,可以简单的添加拷贝的对象方法,标准的解决方案是使用Cloneable修改代码,或者实现自定义的拷贝方法.

浅拷贝:


public class ShallowCopy {
  static   Unsafe unsafe = UnsafeUtils.getUnsafe();
   public static Object shallowCopy(Object obj) {
        long size = sizeOf(obj);
        long start = toAddress(obj);
        long address = unsafe.allocateMemory(size);
        unsafe.copyMemory(start, address, size);
        return fromAddress(address);
    }
//toAddress和fromAddress将对象转换为其在内存中的地址,反之亦然。
    static long toAddress(Object obj) {
        Object[] array = new Object[] {obj};
        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        return normalize(unsafe.getInt(array, baseOffset));
    }

    static Object fromAddress(long address) {
        Object[] array = new Object[] {null};
        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        unsafe.putLong(array, baseOffset, address);
        return array[0];
    }
    public static long sizeOf(Object o) {
    
        HashSet<Field> fields = new HashSet<Field>();
        Class c = o.getClass();
        while (c != Object.class) {
            for (Field f : c.getDeclaredFields()) {
                if ((f.getModifiers() & Modifier.STATIC) == 0) {
                    fields.add(f);
                }
            }
            c = c.getSuperclass();
        }

        // get offset
        long maxSize = 0;
        for (Field f : fields) {
            long offset = unsafe.objectFieldOffset(f);
            if (offset > maxSize) {
                maxSize = offset;
            }
        }
        return ((maxSize/8) + 1) * 8;   // padding
    }
    //normalize是一个为了正确内存地址使用,将有符号的int类型强制转换成无符号的long类型的方法。
    private static long normalize(int value) {
        if(value >= 0) return value;
        return (~0L >>> 32) & value;
    }
}

隐藏密码(Hide Password)

Unsafe类中可以删除内存中的对象,比如密码信息。用户的密码大多数都是byte[]或char[]数组,为什么要使用数组呢?

这个是出于安全的考虑,因为我们可以删除不需要的数组元素,如果是一个字符串对象的话,这可以像一个对象在内存中保存,删除该对象只是删除了引用,真正的数据还保存在内存中。


    String password = new String("l00k@myHor$e");
    String fake = new String(password.replaceAll(".", "?"));
    System.out.println(password); // l00k@myHor$e
    System.out.println(fake); // ????????????

    unsafe.copyMemory(
            fake, 0L, null, toAddress(password), sizeOf(password));

    System.out.println(password); // ????????????
    System.out.println(fake); // ????????????

动态类(Dynamic classes)

我们可以在运行时创建一个类,比如从已编译的.class文件中。将类内容读取为字节数组,并正确地传递给defineClass方法。可以动态调用java的class文件

  byte[] classContents = getClassContent();
    Class c = unsafe.defineClass(null, classContents, 0, classContents.length,null,null);
    Object res = c.getMethod("test").invoke(c.newInstance(), null);// 1
    System.out.println(res);

    private static byte[] getClassContent() throws Exception {
        File f = new File("D:\\work\\open\\rzframe\\rz-common\\src\\test\\java\\com\\rz\\frame\\cas\\AppTest.class");
        FileInputStream input = new FileInputStream(f);
        byte[] content = new byte[(int)f.length()];
        input.read(content);
        input.close();
        return content;
    }

大数组(Big Arrays)

正如你所知,Java数组大小的最大值为Integer.MAX_VALUE。使用直接内存分配,我们创建的数组大小受限于堆大小。

class SuperArray {
    private final static int BYTE = 1;

    private long size;
    private long address;

    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }
}

简单的用法:

long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:" + array.size()); // 4294967294
for (int i = 0; i < 100; i++) {
    array.set((long)Integer.MAX_VALUE + i, (byte)3);
    sum += array.get((long)Integer.MAX_VALUE + i);
}
System.out.println("Sum of 100 elements:" + sum);  // 300

实际上,这是堆外内存(off-heap memory)技术,在java.nio包中部分可用。

这种方式的内存分配不在堆上,且不受GC管理,所以必须小心Unsafe.freeMemory()的使用。它也不执行任何边界检查,所以任何非法访问可能会导致JVM崩溃。

这可用于数学计算,代码可操作大数组的数据,可打破GC在大数组上延迟的限制。

(本文完)

作者:付威

博客地址:http://blog.laofu.online

如果觉得对您有帮助,可以下方的RSS订阅,谢谢合作

如有任何知识产权、版权问题或理论错误,还请指正。

本文是付威的网络博客原创,自由转载-非商用-非衍生-保持署名,请遵循:创意共享3.0许可证

交流请加群113249828: 点击加群   或发我邮件 laofu_online@163.com

付威

获得最新的博主文章,请关注上方公众号