Первая часть расписана вот тут:
Unsafe является "запретным" классом использование которого строго не рекомендуется, более того данный класс может очень сильно различатся в зависимости от виртуальной машины, версии и целевой платформы.
То, что работает в hotspot 1.4 на виндоус может не работать на линуксе или на версии 1.5, разрядность и некоторые параметры виртуальной машины тоже имеют значение, еще имеет значение то, каким образом организована память целевой машины.
Данные ограничения нарушают принцип языка "Write once, run anywhere", так как код зависит от версии и платформы.
- Перед тем как начинать работу с Unsafe необходимо получить инстанс этого класса, конструктор приватный, метод получения инстанса хочет, чтобы класслоадер для вызывающего класса был бутстрап, соответственно просто так не получить.
Классическим методом считается получение инстанса через чтение приватного поля:
static Unsafe unsafe = null;
static {
try
{
Field f = Class.forName("sun.misc.Unsafe").getDeclaredFields()[0];
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
}catch(Throwable t){}
};
Вместо поля можно использовать конструктор (класс не является нативным, если вы создадите новый инстанс он будет работать как надо).
Кстати, unsafe импортируется очень и очень много куда, например можно достать из SharedSecrets (slot 0) или AtomicInteger (slot 1).
После того как вы получили инстанс Unsafe можно начинать эксперементы...
- Получаем поля без проверок безопасности и некоторой специализацией под Minecraft:
private static Field forName_Field(Class Source, String... Names)
{
Field[] f = Source.getDeclaredFields();
for (Field f0 : f)
{
for (String s0 : Names)
{
if (f0.getName().equals(s0))
return f0;
}
}
if (Source.getSuperclass() == null)
return null;
return forName_Field(Source.getSuperclass(),Names);
}
Представленный выше метод получает поле по нескольким именам, это необходимо, чтобы код работал в SRG, FORGE и VANILLA окружении, кроме того вы можете предусмотреть совместимость с несколькими версиями забив дополнительные имена полей, которые присутствуют в других версиях.
Для тех кто по каким либо причинам не любит рекурсию, ранее метод выглядел вот так:
static Field getField(Object Owner, String... Names)
{
Class clz = (Class) ((Owner instanceof Class) ? Owner : Owner.getClass());
for(;;)
{
Field[] fl = clz.getDeclaredFields();
for (Field OUT : fl){
for (String INS : Names){
if (!OUT.getName().equals(INS))
continue;
return OUT;
}
}
clz = clz.getSuperclass();
if (clz == null)
break;
}
return null;
}
- После того как поле получено, можно попробовать его установить, при использовании стандартных методов рефлексии вам потребуется устанавливать флаг overide через setAccessible(true), при использовании Unsafe такой необходимости нет:
static public void setField_int (Object Owner,Field Target,int I) throws Throwable
{
Object o =(Target.getModifiers() & 8) != 0 ? unsafe.staticFieldBase(Target) : Owner;
if ((Target.getModifiers() & 8) != 0)
unsafe.putInt(o,unsafe.staticFieldOffset(Target),I);
if ((Target.getModifiers() & 8) == 0)
unsafe.putInt(o,unsafe.objectFieldOffset(Target),I);
}
Пока я дописываю статью, предлагаю Вам поиграться вот с таким кодом:
UnsafeImpl o = new UnsafeImpl();
Class c = o.getClass();
Field f = getField(c,"TEST3");
Field ff = getField(f,"slot");
Field fff = getField(f,"modifiers");
setField_int(ff,fff,1);
setField_int(f,ff,1);
//ff.setAccessible(true);
System.out.println(ff.getInt(f));
понимание его работы потребуется позже.
- Я уже говорил, что использование Unsafe ставит Вас в зависимость от платформы, как именно вам расскажут результаты выполнения следующего кода:
At A = new UnsafeImpl().new At();
At B = new UnsafeImpl().new At();
for (int i = 0; i < 64; i++){
if (i % 4 == 0)
System.out.println();
System.out.print(unsafe.getByte(A,i) + " ");
}
At пустой nested класс, можно использовать чтото другое, например Integer и String
11 22 33 44 это торчат поля класса хозяина.
Первый запуск 64 бит без конфигурации, конфигурация -XX:+UseCompressedOops к сожалению не дала других результатов (а вот -XX:-UseCompressedOops дала), но суть в том, что при сжатых заголовках некоторые поля размером 4 байта, а некоторые 8, без сжатых заголовках все поля 8 байт, сжатые заголовки включаются по умолчанию (во всяком случае у меня);
1 0 0 0
0 0 0 0
-23 32 121 -33
31 44 94 -11
1 0 0 0
0 0 0 0
-126 22 121 -33
11 0 0 0
22 0 0 0
33 0 0 0
44 0 0 0
0 0 0 0
1 0 0 0
0 0 0 0
72 22 116 -33
38 44 94 -11
32 бита
1 0 0 0
32 66 54 52
-128 18 57 36
0 0 0 0
1 0 0 0
-64 58 54 52
11 0 0 0
22 0 0 0
33 0 0 0
44 0 0 0
1 0 0 0
-88 9 -44 56
-80 18 57 36
0 0 0 0
0 0 0 0
0 0 0 0
32 бита ява 5
1 0 0 0
0 0 0 0
-5 64 121 -33
76 -28 93 -11
1 0 0 0
0 0 0 0
-75 53 121 -33
11 0 0 0
22 0 0 0
33 0 0 0
44 0 0 0
0 0 0 0
1 0 0 0
0 0 0 0
-105 36 116 -33
84 -28 93 -11
Как вы видите, картина несколько различается, и если использовать оффсеты для другой версии, можно получить неясные глюки или вовсе вылет JVM, например если вы захотите получать указатель класса по оффсету, а там не класс а данные поля.
- Меняем класс объекта в рантайме...
Слегка подкорректированный вариант кода:
At A = new At();
At B = new At();
At B1 = new At();
At B2 = new At();
for (int i = 0; i < 64; i++){
if (i % 4 == 0)
System.out.println();
System.out.print(unsafe.getByte(A,i) + " ");
}
Класс At больше не nested, а вполне самостоятельный.
Выход будет вот такой:
1 0 0 0
0 0 0 0
-59 32 121 -33
0 0 0 0
1 0 0 0
0 0 0 0
-59 32 121 -33
0 0 0 0
1 0 0 0
0 0 0 0
-59 32 121 -33
0 0 0 0
1 0 0 0
0 0 0 0
-59 32 121 -33
0 0 0 0
Даже без чтения исходников hotspot, становится очевидно, что инстанс объекта описан следующим набором байт:
1 0 0 0
0 0 0 0
-59 32 121 -33
0 0 0 0
На 32х битах будет выход:
1 0 0 0
24 65 54 52
1 0 0 0
24 65 54 52
1 0 0 0
24 65 54 52
1 0 0 0
24 65 54 52
1 0 0 0
-88 9 -44 56
-72 14 57 36
0 0 0 0
0 0 0 0
0 0 0 0
1 0 0 0
-16 62 55 57
один инстанс
1 0 0 0
24 65 54 52
Это связано с тем, что все объекты выраниваются по границе в 4 а для 64х бит по границе 8, то есть в 64 битной яве пустой класс без полей начинает занимать на 100% больше памяти, просто потому что это 64х битная ява, для больших классов это не важно, а вот для 100500 мелких набегает немаленький оверхеад.
Так что если ваша утилита хорошо шла на гигабайте памяти, то переход на 64 бита может внезапно потребовать 2 гигабайта.
Что нам дают эти байты? всё просто, мы можем их менять, можем взять и перезаписать класс объекта.
- Supercast - меняем класс объекта в рантайм без пересоздания этого самого объекта, может потребоваться много где;
Создаём два класса:
class At{
String PAYLOAD = " NULL ";
public At(String Payload){
this.PAYLOAD = Payload;
}
public void action(){
System.out.println("ACTION" + PAYLOAD);
}
}
class Bt{
String PAYLOAD = " NULL ";
public Bt(String Payload){
this.PAYLOAD = Payload;
}
public void action(){
System.out.println(PAYLOAD + "ACTION");
}
}
Классы идентичны кроме одного метода, который мы кстати и будем вызывать для теста, Payload сделан не константой, чтобы было проще определить, где у нас класс, а где поле payload:
1 0 0 0
0 0 0 0
-29 32 121 -33
-51 43 94 -11
1 0 0 0
0 0 0 0
-29 32 121 -33
-112 40 94 -11
Unsafe даёт возможность устанавливать значения типа byte int и long, что соответствует слотам 1 4 и 8, апгрейдим код:
At A = new At("TEST");
At A1 = new At("BLABLA");
//Bt B = new Bt("");
//class changing magic!
long classpointer = unsafe.getLong(A,8);
System.out.println(Long.toHexString(classpointer));
//unsafe.putInt(B, 8, classpointer);
//A.action();
//B.action();
for (int i = 0; i < 8*2; i++){
if (i % 4 == 0)
System.out.println();
System.out.print(String.format("%02X ", unsafe.getByte(A,i)));
}
На выходе получаем
f55e2bdadf7920f1
01 00 00 00
00 00 00 00
F1 20 79 DF
DA 2B 5E F5
Приходит понимание того, что брать Long это ошибка, так как в этом случае мы захватываем в том числе указатель на payload который нам трогать совершенно нет никакой необходимости, если выключить сжатие oop то кроме как лонг брать не выйдет, а вот с выключенным сжатием следует брать int.
At A = new At("TEST");
Bt B = new Bt("BLABLA");
int classpointer = unsafe.getInt(A,8);
System.out.println(Integer.toHexString(classpointer));
unsafe.putInt(B, 8, classpointer);
A.action();
B.action();
System.out.println(B.getClass());
на выходе даст
df7920e4
ACTIONTEST
ACTIONBLABLA
class ru.rawcode.dev.At
Соответственно мы успешно изменили класс объекта B на класс объекта А;
Теперь посмотрим, что будет если классы разные, чтобы было не так печально, строку заменим на примитивный тип int а из одного класса это поле уберём;
class At{
long PAYLOAD = 10;
public void action(){
System.out.println("ACTION" + PAYLOAD);
}
}
class Bt{
public void action(){
System.out.println("ACTION BETA");
}
}
Класс Bt поля PAYLOAD не имеет, посмотрим что будет если мы преобразуем класс Bt в класс At и вызовем метод:
At A = new At();
Bt B = new Bt();
int classpointer = unsafe.getInt(A,8);
System.out.println(Integer.toHexString(classpointer));
unsafe.putInt(B, 8, classpointer);
A.action();
B.action();
System.out.println(B.getClass());
for (int i = 0; i < 8*3; i++){
if (i % 4 == 0)
System.out.println();
System.out.print(String.format("%02X ", unsafe.getByte(B,i)));
}
на выходе даст:
df792116
ACTION10
ACTION1
class ru.rawcode.dev.At
01 00 00 00
00 00 00 00
16 21 79 DF
00 00 00 00
01 00 00 00
00 00 00 00
Как мы видим на выходе взялась единичка, эта единичка является куском следующего объекта, который лежит дальше в куче, следующий код наглядно покажет это:
At A = new At();
Bt B = new Bt();
At zA = new At();
unsafe.putInt(zA, 0,9999999);
int classpointer = unsafe.getInt(A,8);
System.out.println(Integer.toHexString(classpointer));
unsafe.putInt(B, 8, classpointer);
A.action();
B.action();
System.out.println(B.getClass());
for (int i = 0; i < 8*4; i++){
if (i % 4 == 0)
System.out.println();
System.out.print(String.format("%02X ", unsafe.getByte(B,i)));
}
df79211e
ACTION10
ACTION9999999
class ru.rawcode.dev.At
01 00 00 00
00 00 00 00
1E 21 79 DF
00 00 00 00
7F 96 98 00
00 00 00 00
1E 21 79 DF
00 00 00 00
Соответственно изменяя класс объекта, на не очень подходящий, мы можем наломать дров очень и очень много., поскольку JVM дополнительных проверок на целостность не делает в принципе, поля читаются с памяти напрямую и если память не относится к полю, писать или читать с неё не очень безопасно.