Unsafe Java I - Небезопасная жаба

Дата публикации 20 май 2006

Unsafe Java I - Небезопасная жаба — Архив WASM.RU

  1. Класс sun.misc.Unsafe
  2. Структуры виртуальной машины
  3. Особенности версии 1.5
  4. Применение на практике
  5. Бесконечный final

Как известно при разработке языка Java с самого начала делался упор на "безопасность" кода (так называемый "safe code"). Помимо всего прочего это означало отказ от указателей, работы с памятью и тому подобных низкоуровневых средств. Совсем отказаться правда не удалось, пришлось оставить лазейку, в первую очередь естественно для собственных классов. Но все, что использует Java Runtime, можем использовать и мы. В этой статье мы научимся писать небезопасный код на Яве и используем новоприобретенные знания для решения некоторых интересных проблем, которые штатными средствами Явы не решаются.

Рассматривать мы будем только Sun'овскую виртуальную машину, по двум причинам. Во-первых она применяется наиболее широко и является своеобразным эталоном. Во-вторых до IBM'овской у меня руки еще не дошли, а больше никаких (реально использующихся) я не знаю. Все приведенные ниже примеры протестированы с Java 1.4.2_11 и 1.5.0_06. Предполагается что читатель достаточно хорошо разбирается как в Яве, так и в общих принципах программирования.

1. Класс sun.misc.Unsafe

Малоизвестный класс sun.misc.Unsafe входит в комплект Sun Java Runtime начиная с первых версий. Как и все остальные классы в package sun.*, Unsafe не документирован, но имена (в большинстве своем нативных) функций, видимые при декомпиляции, говорят сами за себя. Явно присутствуют функции работы с памятью (allocateMemory, freeMemory,...), чтения и записи значений по заданному адресу(putLong, getLong,...) и некоторые более специализированные(throwException, monitorEnter,...). То есть в принципе все, что нам нужно.

Правда так просто инстанциировать Unsafe не удастся. Единственный constructor - приватный, а в getUnsafe() проверяется загрузчик вызвавшего класса и объект возвращается только если класс загружен Bootloader'ом. В противном случае получаем SecurityException.

Код (Text):
  1.  
  2.     public static Unsafe getUnsafe()
  3.     {
  4.         Class class1 = Reflection.getCallerClass(2);
  5.         if(class1.getClassLoader() != null)
  6.             throw new SecurityException("Unsafe");
  7.         else
  8.             return theUnsafe;
  9.     }
  10.  

К счастью существует еще внутренняя переменная theUnsafe, до которой мы можем добраться с помощью Reflection. Всю черновую работу соберем в один класс (назовем его UnsafeUtil), который будем расширять по мере надобности.

Код (Text):
  1.  
  2. public class UnsafeUtil {
  3.  
  4.     public static Unsafe unsafe;
  5.     private static long fieldOffset;
  6.     private static UnsafeUtil instance = new UnsafeUtil();
  7.  
  8.     private Object obj;
  9.    
  10.     static {
  11.         try {
  12.             Field f = Unsafe.class.getDeclaredField("theUnsafe");
  13.             f.setAccessible(true);
  14.    
  15.             unsafe = (Unsafe)f.get(null);
  16.             fieldOffset = unsafe.objectFieldOffset(UnsafeUtil.class.getDeclaredField("obj"));
  17.         } catch (Exception ex) {
  18.             throw new RuntimeException(e);
  19.         }
  20.     };
  21. }
  22.  

Конечно можно просто внести UnsafeUtil в список загружаемых Bootloader'ом классов (указав путь в ключе -Xbootclasspath/a) и вызывать getUnsafe() в соответствии с замыслом Sun. Беда в том, что тогда все использующие UnsafeUtil классы также должны быть прописаны в bootclasspath'е (см. главу "5.3 Creation and Loading" в VM spec). Правда например package java.nio как-то ухитряется обходить это ограничение, но как именно пока не очень понятно. К тому же этот способ выходит за рамки "чистого" кода, так как требует дополнительных стартовых опций для виртуальной машины. Так что не будем мудрствовать и ограничимся чтением theUnsafe.

В первую очередь нам понадобятся естественно операции референцирования и дереференцирования, ObjectToAddress и AddressToObject соответственно.

Код (Text):
  1.  
  2. public static long ObjectToAddress (Object o){
  3.     instance.obj = o;
  4.     return unsafe.getLong(instance, fieldOffset);
  5. }
  6.  
  7. public static Object AddressToObject (long address){
  8.     unsafe.putLong(instance, fieldOffset, address);
  9.     return instance.obj;
  10. }
  11.  

С ними мы уже достаточно хорошо вооружены в техническом плане, не хватает только информации по внутреннему устройству Явы. Ее мы найдем в следующем разделе.

Очень похожую реализацию кстати сделал Don Schwarz (http://don.schwarz.name/index.php?p=30). Это одно из очень немногих мест, где можно найти хоть какие-то примеры работы с классом Unsafe. К сожалению Don в свое время не оценил потенциал низкоуровнего программирования в Яве и остановился, сделав всего пару робких шагов. Мы же пойдем дальше.

2. Структуры виртуальной машины

Теперь посмотрим, в каком виде виртуальная машина (версии 1.4) хранит данные в памяти. Поскольку Ява работает с классами и их инстанциями, то ими и займемся.

Инстанция:

Код (Text):
  1.  
  2. instance_struct {
  3.     0   magic   // всегда равен 1
  4.     4   class   // указатель на структуру класса, class_struct*
  5.     8   ...     // Дальше идут подряд переменные инстанции(то есть все которые не static),
  6.     12  ...     // порядок пока не очень понятен, судя по всему в порядке объявления и  
  7.     16  ...     // обьекты перед примитивными типами
  8.     ...
  9. }
  10.  

Переменные типа double и long занимают 64 бита, остальные по 32. В памяти инстанции выравниваются по 64-битной границе, дополняются при необходимости нулями. То есть по сути мы имеем обыкновенную сишную структуру плюс указатель на ее описание.

Класс:

Код (Text):
  1.  
  2. class_struct {
  3.     0   magic       // всегда равен 1
  4.     4   class       // class_struct*, указатель на структуру класса более высокого уровня, зачем нужен - непонятно
  5.     8   ...         // значение неизвестно
  6.     12  super_count     // количество уровней наследования: 0x18 - один(наследует от Object), 0х1c - два и т.д. до восьми(0х34), потом 0х10. У интерфейсов тоже 0х10.  
  7.     16      interface       // class_struct*, указатель на какой-либо из интерфейсов класса (на какой именно непонятно), часто просто 0
  8.     20  interface_list  // указатель на массив с элементами типа class_struct*, все интерфейсы класса
  9.     24  ...
  10.     28  ...
  11.     32  ...
  12.     36  ...     // указатели на структуры восьми высших суперклассов начиная от Object и кончая this (если поместится)
  13.     40  ...
  14.     44  ...
  15.     48  ...
  16.     52  ...
  17.     56  size        // размер инстанции класса в DWORD'ах
  18.     60  this_class      // instance_struct*, указатель на инстанцию java.lang.Class соответствующую данному классу
  19.     64  access_flags    // доступ к классу как описано в VM spec ( 0х1 - public, 0х10 - final и т.д.)
  20.     68  ...
  21.     ...
  22. }
  23.  

Здесть я привел только те куски, которые мы будем использовать в дальнейшем и в которых я более или менее уверен. На самом деле class_struct значительно длиннее и содержит кроме того указатели на функции класса, статические переменные и все остальное, что может понадобится виртуальной машине. Все эти структуры по понятным причинам нигде не документированы и разбираться надо вручную - хоть и несложно, но достаточно трудоемко. Если у кого-то есть желание помочь, буду только рад.

3. Особенности версии 1.5

С переходом на последнюю (на момент написания) версию 1.5.0_06 внутренние структуры виртуальной машины претерпели некоторые изменения. К счастью небольшие: изменился в основном порядок полей, значения остались в большинстве прежними. Структура класса выглядит теперь следующим образом:

Код (Text):
  1.  
  2. class_struct_1_5 {
  3.     0   magic       // всегда равен 1
  4.     4   class       // class_struct*, указатель на структуру класса более высокого уровня, зачем нужен - непонятно
  5.     8   ...         // значение неизвестно
  6.     12  size        // размер инстанции класса в DWORD'ах
  7.     16  super_count     // количество уровней наследования: 0x20 - один(наследует от Object), 0х24 - два и т.д. до восьми(0х38), потом 0х14. У интерфейсов тоже 0х14.  
  8.     20      interface       // class_struct*, указатель на какой-либо из интерфейсов класса (на какой именно непонятно), часто просто 0
  9.     24  interface_list  // указатель на массив с элементами типа class_struct*, все интерфейсы класса
  10.     28  ...
  11.     32  ...
  12.     36  ...
  13.     40  ...     // указатели на структуры восьми высших суперклассов начиная от Object и кончая this (если поместится)
  14.     44  ...
  15.     48  ...
  16.     52  ...
  17.     56  ...
  18.     60  this_class      // instance_struct*, указатель на инстанцию java.lang.Class соответствующую данному классу
  19.     64  ...
  20.     68  ...     // точные значения неизвестны
  21.     72  ...
  22.     76  ...
  23.     80  access_flags    // доступ к классу как описано в VM spec ( 0х1 - public, 0х10 - final и т.д.)
  24.     84  ...
  25.     ...
  26. }
  27.  

Обратите внимание на переехавшее вперед поле size и сдвинутые по сравнению с версией 1.4 значения поля super_count. При написании кода придется учитывать подобные мелкие отличия. Поэтому будем в самом начале опрашивать версию виртуальной машины и сохранять результат в переменной vm1_5. Интересно кстати, что версии 1.5.0_0x, x<6 используют все еще старые структуры. То есть достаточно глобальные изменения в виртуальной машине не обязательно приурочены к значительному скачку версии - сам по себе примечательный факт.

4. Применение на практике

Перейдем к практической части и попробуем приспособить теорию к делу. Для начала решим одну проблему, которая существует почти столько же сколько и сама Ява. А именно напишем функцию sizeof() для объектов. Желающие могут использовать свой любимый поисковик и посмотреть(например по Java+sizeof), сколько и каких решений предлагалось за последние годы, от использования Reflection до вычисления размера занятой памяти и деления его на количество объектов. Точного ответа при этом не давало, что интересно, ни одно. Нам же достаточно просто прочитать поле class_struct.size

Код (Text):
  1.  
  2. public static long sizeOf(Object object){
  3.     return unsafe.getAddress(unsafe.getAddress(ObjectToAddress(object)+4)+(vm1_5?12:56));
  4. }
  5.  

Функция возвращает результат в DWORD'ах, если нужен в байтах не забудьте умножить на 4. С помощью sizeOf() можно теперь копировать содержимое инстанций - так называемая "shallow copy". "Shallow" - так как в случае внутренних переменных типа Object (и от него производных) копируются естественно только указатели, а не объекты целиком.

Код (Text):
  1.  
  2. public static void copyObjectShallow(Object objectSource, Object objectDest) {
  3.     unsafe.copyMemory(ObjectToAddress(objectSource),ObjectToAddress(objectDest),sizeOf(objectSource)*4);
  4. }
  5.  

copyObjectShallow бывает особенно полезна если иметь дело с объектами, содержащими большое количество переменных примитивных типов. То есть когда класс используется в основном для хранения данных, как структура в С. Копировать переменные по одной (единственный штатный способ Явы) - удовольствия мало.

Как известно, приведение типов в Яве осуществляется динамически, с учетом иерархии классов. Бинарного каста (reinterpret_cast в терминах С++) Ява к сожалению не поддерживает. Заполним этот пробел.

Код (Text):
  1.  
  2. public static Object reinterpret_cast(Object o, Class cl){
  3.     unsafe.putAddress(ObjectToAddress(o)+4,unsafe.getAddress(ObjectToAddress(cl)+8));
  4.     return o;
  5. }
  6.  

reinterpret_cast возвращает указатель на объект о, приведенный к заданному через параметер cl типу. Имеет ли такое преобразование смысл, должен как всегда решать сам пользователь. Не следует только забывать, что ошибка при подобных манипуляциях с памятью почти всегда вызовет не безобидную Java Exception, а что-нибудь вроде Access Violation в виртуальной машине. Классический случай применения reinterpret_cast - когда один и тот же класс загружается два раза двумя разными ClassLoader'ами.

Если собираетесь писать многопоточную программу, не забывайте о синхронизации. Например имеет смысл добавить в определения приведенных выше функций слово synchronized. Иначе может случиться так, что разные потоки попытаются одновременно изменять структуры классов и радости тогда будет много.

Исходный код класса UnsafeUtil вместе с примерами использования отдельных его функций находится в приложении к статье.

5. Бесконечный final

Чтобы лучше оценить те практически неограниченные возможности, которые открывает перед нами манипулирование структурами классов, разберем пример посложнее.

Как известно, "final" в объявлении класса запрещает наследование от него. Например классы типов, такие как String, Integer и т.д. умышленно сделаны разработчиками языка конечными. Новички с завидным постоянством спрашивают на форумах, можно ли написать собственный класс строк, наследующий от String, и с таким же постоянством получают ответ "нельзя". И тем не менее правильный ответ - можно.

Для простоты и наглядности возьмем функцию hashCode(). Как известно хэш строк вычисляется по алгоритму

Код (Text):
  1.  
  2.     s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
  3.  

где n - длина строки и s[i] - i-й символ. Предположим теперь, что нас не устраивает стандартный алгоритм и мы хотим вычислять сумму не с начала строки, а с конца. Вот так:

Код (Text):
  1.  
  2.     s[n-1]*31^(n-1) + s[n-2]*31^(n-2) + ... + s[0]
  3.  

То есть нужен класс, который наследует от String и имплементирует новую hashCode(). И именно эти свойства имеет класс MagicString, который можно найти в директории /string в исходниках к статье. MagicStringWthStub реализует ту же самую идею, только чуть элегантнее, например без использования Reflection. Недостатком в этом случае является необходимость написания stub'а, что впрочем можно легко автоматизировать. MagicString обходится без дополнительных классов.

Код самих классов я здесь приводить не хочу, чтобы не загромождать статью. Основная идея состоит в том, что мы вносим String в список суперклассов(смещения 24-52 + поле super_count) и подправляем поле access_flags нужным образом (убираем final). Детали реализации можно посмотреть в приложенных исходниках. Проверим теперь MagicString в действии:

Код (Text):
  1.  
  2.         String s = (String)(Object)(new MagicStringWithStub("AB"));
  3.         System.out.println("Magic String hashcode: "+s.hashCode());
  4.         String s1 = new String("AB");
  5.         System.out.println("Standard String hashcode: "+s1.hashCode());
  6.  

На выходе получим

Код (Text):
  1.  
  2.     Magic String hashcode: 2111
  3.     Standard String hashcode: 2081
  4.  

Как видим, единственное небольшое неудобство состоит в том, что кастить в String приходится через Object. Понятно почему: компилятор-то о наших играх ничего не знает. В остальном мы полностью достигли цели: получили String с нестандартным хэш-кодом.

В следующей статье (при условии, что у меня дойдут руки ее написать :smile3:) мы научимся создавать на Яве самомодифицирующийся код.

Благодарности

Quantum - за вдумчивое и терпеливое рецензирование черновых вариантов статьи. Если я не последовал каким-либо его советам, то исключительно по причине собственной лени.

Приложение

unsafe_java_1_code.zip (7 KB) - Примеры к статье © Stiver

0 2.266
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532