Java: class-файл. Копнём глубже

в 9:17, , рубрики: class, java, метки: ,

image
Здравствуйте, читатели.

Понадобилось мне однажды внести небольшое изменение в программу (пожелавшую остаться неизвестной), чтобы избавить её от досадного глюка. Совсем копеечное, поменять порядок вызова функций. Перед этим я довольно долго переписывался с авторами, детально описал им причины возникновения ошибки и способы воспроизведения, даже видео присылал. Но то ли английский мой настолько плох, что не понимали они, то ли просто поленились (странный порядок вызова функций провоцировал появление ошибок в подключаемых модулях. Это не наша проблема, это проблема авторов тех модулей).

В общем, ни багфикса, ни исходников… Программа-то на яве, но обработана злобным протектором, переместившим примерно половину классов в пакет по умолчанию (такой код скомпилировать нельзя, но он выполняется нормально) и присвоившим им одинаковые имена в разном регистре. Про имена переменных и функций вообще молчу.

Декомпилятор мне не помощник, 1000 с хвостиком файлов с убийственной архитектурой я не потяну. Ну что же, — подумал я, — мы пойдём другим путём. Есть несколько софтин, редактирующих классы. Есть небольшой опыт ковыряния кода. Есть описания инструкций. Казалось бы, в чём проблема?

Ан нет. Программа скомпилирована под 7 версию java. Редакторы, которые я нашёл, в последний раз обновилялись несколько лет назад, но до сих пор исправно работали. А в этот раз я получил неработающий класс и горстку бесполезных сообщений об ошибках в крашрепорте одной из софтин. Выяснилось, что, кроме всего прочего, Jasmin код, полученный при помощи Javap, компилирует некорректно.

Что нам стоит дом построить? Я решил не мелочиться и написать свой редактор. Или хотя бы парсер class-файлов, о процессе создания которого и написана эта статья.

Осторожно, под катом могут быть велосипеды с квадратными колёсами.

Итак. С чего начать? Начнём, пожалуй, с посещения сайта Oracle и выкачивания вот этого замечательного документа.

На первый взгляд, ничего сложного нет:

Общий формат файла

ClassFile {
 u4 magic; - то самое число, 0xCAFEBABE
 u2 minor_version;  
 u2 major_version;
 u2 constant_pool_count;
 cp_info constant_pool[constant_pool_count-1]; - пул констант
 u2 access_flags; 
 u2 this_class;
 u2 super_class;
 u2 interfaces_count; 
 u2 interfaces[interfaces_count]; - интерфейсы 
 u2 fields_count;
 field_info fields[fields_count]; - поля
 u2 methods_count;
 method_info methods[methods_count]; - методы
 u2 attributes_count;
 attribute_info attributes[attributes_count]; - атрибуты
}

Сокращениями un обозначены размеры записей в файле. u4 — 4 байта, u2 — 2 байта, u1, очевидно, 1 байт. Все числа должны храниться в беззнаковом формате (я об это обжёгся в самом начале).

Думаю, стоит немного подробнее описать эту структуру.
Итак, magic – «магическое» число, сигнатура, по которой java-машина опознаёт этот файл.
minor_version и major_version – версия файла.
constant_pool_count – количество констант (см. следующий пункт)
cp_info[] – пул констант. В нём хранятся все строки, числа, типы (забегая вперёд, скажу, что тип объекта (и примитивных типов тоже), принимаемого/возвращаемого функцией, в конечном счёте хранится в строке) и прочие объекты.
access_flags – флаги доступа. Private, public, а также final и прочие… Всего 2 байта обеспечивают хранение важнейшей для ООП информации :)
this_class, super_class – всего лишь номера двух констант типа class_info в пуле констант, но совершенно необходимые. Названия их говорят за себя.
interfaces_count – количество интерфейсов, которые реализует данный класс (см. следующий пункт).
interfaces[] – список интерфейсов. Точнее, список номеров констант в пуле. Тип этих констант – тоже class_info.
fields_count – количество полей класса
method_info[] – Поля класса.
methods_count – количество методов.
method_info[] – методы
attributes_count – количество атрибутов класса
attribute_info[] – атрибуты класса. Например, название файла с исходным кодом.

Рассмотрим нетривиальные элементы этого списка:

Пул констант.

Пул констант – массив элементов пула. Следует отметить, что нумерация элементов начинается с 1. Есть несколько типов констант (полный список Вы можете найти на стр. 83 спецификации). Каждая константа представлена следующей структурой:

cp_info {
 u1 tag;
 u1 info[];
}

tag – байт, указывающий на тип константы. Например, типу Integer_info соответствует число 3, а String_info — 8.
info – данные. Количество полей, принадлежащих разным константам, различается. Например, String_info содержит только tag и двухбайтовый string_index, являющийся номером константы типа Utf8_info. Utf8_info, в свою очередь, содержит тэг, определяющий тип константы, длину строки и данные:

CONSTANT_Utf8_info {
 u1 tag;
 u2 length;
 u1 bytes[length];
}

Очевидно, что размеры этих констант различаются.

В способе организации пула констант есть одна интересная особенность: если константа имеет тип long или double, она занимает 2 ячейки пула. Это связано с организацией работы стека, и выходит за рамки данной статьи (Вы же не хотите, чтобы я перепечатал вам всю спецификацию? ;-) ).

Я решил, что, поскольку элементов пула много, и они все разные, каждый элемент должен самостоятельно загружаться из входного потока данных (и не менее самостоятельно сохранять своё содержимое). Пришлось всё же создать класс PoolEleemntLoader, который осуществлял считывание первого байта атрибута, определение его типа и конструирование объекта, но дальше управление передавалось методам объекта. Пример одного из классов:

Скрытый текст

public class CONSTANT_Class  extends CONSTANTPoolElement
{
    private int name_index; // ссылка на utf8 с описанием класса (short)

    public int getName_index() 
    {
        return name_index;
    }

    public void setName_index(int name_index) 
    {
        this.name_index = name_index;
    }

    @Override
    public int getTag() 
    {
        return 7;
    }

    @Override
    public void selfLoad(DataInputStream mainInput) throws IOException 
    {
        name_index = mainInput.readUnsignedShort();
    }

    @Override
    public void selfSave(DataOutputStream mainOutput) throws IOException
    {
        mainOutput.writeByte(getTag());
        mainOutput.writeShort(name_index);
    }
}

Поля класса

field_info {
 u2 access_flags;
 u2 name_index;
 u2 descriptor_index;
 u2 attributes_count;
attribute_info attributes[attributes_count];
}

access_flags – флаги доступа
name_index – ссылка на константу, хранящую имя поля в формате UTF-8. Кстати, способ записи текста в class-файле отличается от стандартного способа записи в этой кодировке. Интересующиеся могут найти различия на стр. 90.
descriptor_index – строка, хранящая тип поля.
Есть несколько специальных символов, обозначающих его: примитивные типы, массив, объект.
Примитивные типы обычно обозначаются первой буквой своего названия. Например, Integer – “I”, byte – B. Но long – J. Массив обозначает скобка «[», стоящая перед типом. Например, int[] – это [I, byte[][] будет записано как [[b. Это применимо и к объектам.
Объект обозначается чуть сложнее. Это – строка, состоящая из 3 частей:
-символ «L»
-имя класса
-символ «;»
Полное имя класса, записывается через «/». Например, java/lang/Object. Это так называемая «внутренняя форма записи», о которой Вы можете прочитать на стр. 75.
Последний пример: массив String[] будет записан как “[Ljava/lang/String;”. Максимальная длина типа равна 255 символам. 255 символов должно быть достаточно для каждого :)
attribute_info[] – массив с атрибутами поля. Будет описан чуть ниже.

Работу с полями я организовал точно так же, но здесь структура уже более ложная и комментариев меньше =)

Скрытый текст

public class Method
{
    private short access_flags;
    private short name_index;
    private short descriptor_index;
    private short attributes_count;
    private AbstractAttribute attributes[];

    public short getAccess_flags()
    {
        return access_flags;
    }

    public void setAccess_flags(short access_flags)
    {
        this.access_flags = access_flags;
    }

    public short getName_index()
    {
        return name_index;
    }

    public void setName_index(short name_index)
    {
        this.name_index = name_index;
    }

    public short getDescriptor_index()
    {
        return descriptor_index;
    }

    public void setDescriptor_index(short descriptor_index)
    {
        this.descriptor_index = descriptor_index;
    }

    public short getAttributes_count()
    {
        return attributes_count;
    }

    public void setAttributes_count(short attributes_count)
    {
        this.attributes_count = attributes_count;
    }

    public AbstractAttribute[] getAttributes()
    {
        return attributes;
    }

    public void setAttributes(AbstractAttribute[] attributes)
    {
        this.attributes = attributes;
    }
    
    public void selfLoad(DataInputStream mainInput, CONSTANTPoolElement pool[])
            throws IOException, FrameException
    {
        access_flags = mainInput.readShort();
        name_index = mainInput.readShort();
        descriptor_index = mainInput.readShort();
        attributes_count = mainInput.readShort();
        if (System.getProperty("debug") != null && System.getProperty("debug").equalsIgnoreCase("true"))
            System.out.println("Loading method      " + ((CONSTANT_Utf8)(pool[name_index])).getString());
        
        attributes = AttributeLoader.loadElements(mainInput, pool, attributes_count);
    }
    
    public void selfSave(DataOutputStream mainOutput) throws IOException, FrameException
    {
        mainOutput.writeShort(access_flags);
        mainOutput.writeShort(name_index);
        mainOutput.writeShort(descriptor_index);
        mainOutput.writeShort(attributes_count);
        AttributeSaver.saveElements(attributes, mainOutput);
        
    }
}

Методы
methods[] – массив структур method_info, очень похожих на field_info:

 method_info { 
 u2 access_flags;
 u2 name_index;
 u2 descriptor_index;
 u2 attributes_count;
 attribute_info attributes[attributes_count];

descriptor_index – номер константы (тоже utf_8) с описанием типов принимаемых и возвращаемых значений. Записывается в следующем виде (извините, я не знаю, где лучше рисовать синтаксические диаграммы; буду очень благодарен, если подскажете):
image

Например, (ILjava.lang.String;[b)I – это int func(int, String, byte[]), а ()v – void func().
attributes[] – массив с атрибутами метода. Очевидно, что коду метода больше негде храниться. Действительно, его содержит атрибут code, описанный в следующем разделе.
Атрибуты
А вот эта структура оказалась достаточно сложной из-за того, что, во-первых, имеет много уровней вложенности и, во-вторых, является частично рекурсивной. Ктому же, каждый атрибут имеет в своём описании собственный размер. Это сделано для возможности пропуска атрибутов, которые не поддерживает java-машина.
Общая структура атрибута:

attribute_info {
 u2 attribute_name_index;
 u4 attribute_length;
 u1 info[attribute_length];
}

attribute_name_index – имя атрибута. Точнее, ссылка на константу с его названием.
attribute_length – длина атрибута в байтах. Должна пересчитываться при изменении вложенных атрибутов, если такие есть.
info[] – данные, которые различны для разных атрибутов.

Я приведу несколько примеров, полный список Вы можете найти на странице 100:

”ConstantValue”

ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}

Этот атрибут хранит ссылку на единственную константу. Просто? Тогда посмотрите на это :)

”Code”

Code_attribute {
 u2 attribute_name_index;
 u4 attribute_length;
 u2 max_stack;
 u2 max_locals;
 u4 code_length;
 u1 code[code_length];
 u2 exception_table_length;
 { u2 start_pc;
  u2 end_pc;
  u2 handler_pc;
  u2 catch_type;
 } exception_table[exception_table_length];
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}

Как видите, код лежит в самом обычном массиве байт. Max_stack и max_locals – максимальное количество локальных переменных и максимальная глубина стека, доступные этому методу.

attribute_name_index в данном случае указывает на константу типа utf8_info с текстом «Code» — с названием атрибута.

Таблица исключений сделана довольно-таки просто: каждое исключение имеет указатель на тип объекта и 2 смещения, ограничивающие диапазон его действия. Кроме того атрибут Code может содержать вложенный атрибут Exceptions_attribute, содержащий список исключений, выбрасываемых данным методом.

Очень показательным примером важности атрибутов является атрибут Deprecated.
Это – атрибут – флаг, он не содержит каких-либо дополнительных полей.

Deprecated_attribute {
 u2 attribute_name_index;
 u4 attribute_length;
}

Да, аннотации тоже хранятся в виде атрибутов.
Существует множество атрибутов, и их список постоянно увеличивается. Например, не так давно (java6) был добавлен атрибут StackMapTable, нужный для верификации типов операндов. Этот атрибут существует только в связке с атрибутом Сode. Точнее, внутри него. Упрощённо говоря, он содержит список адресов из массива code атрибута Code и список допустимых типов данных для локальных переменных и стека. Структура этого атрибута сложна, он состоит из так называемых кадров (stack_map_frames), которые бывают нескольких типов. Пример кода, обрабатывающего один из таких кадров, Вы можете увидеть ниже.

Скрытый текст

public class append_frame extends AbstractStackMapFrame // Извините за кириллицу, делал для себя…
{
    private int frame_type; // byte
    private int offset_delta; //(short)
    private AbstractTypeInfo locals[];

    @Override
    public int getFrame_type()
    {
        return frame_type;
    }

    @Override
    public void setFrame_type(int frame_type) throws FrameException 
    {
        if (frame_type < 252 || frame_type > 254)
            throw new FrameException("Недопустимый тег фрейма: " + frame_type);
        this.frame_type = frame_type;
    }

    public int getOffset_delta()
    {
        return offset_delta;
    }

    public void setOffset_delta(int offset_delta)
    {
        this.offset_delta = offset_delta;
    }
    
    public AbstractTypeInfo[] getLocals()
    {
        return locals;
    }

    public void setLocals(AbstractTypeInfo[] locals) throws FrameException
    {
        if (locals.length != frame_type - 251)
            throw new FrameException("Неверная размерность");
        this.locals = locals;
    }
    
    @Override
    public int getRealLength()
    {
        int result = 1;
        result += 2;
        for (int i = 0; i < frame_type - 251; i++)
        {
            result += locals[i].getRealLength();
        }
        return result;
    }

    @Override
    public void selfLoad(DataInputStream mainInput) throws IOException
    {
        offset_delta = mainInput.readUnsignedShort();
        locals = TypeInfoLoader.loadElements(mainInput, frame_type - 251);
    } 
    
    @Override
    public void selfSave(DataOutputStream mainOutput) throws IOException
    {
        mainOutput.writeByte(frame_type);
        mainOutput.writeShort(offset_delta);
        TypeInfoSaver.saveElements(locals, mainOutput);
    }
}

В итоге, отредактировал я тот код. Написал несколько одноразовых программ, которые, используя написанный парсер, перенесли все классы из пакета по умолчанию в пакет с заданным именем. После компиляции точно так же исправил имена классов, шло обращение к пакету по умолчанию. Все рады и счастливы.
А разработчику тому желаю быть повнимательнее и прислушиваться к словам окружающих.

Исходные коды доступны здесь. Кроме парсера самого блока code, его я в спешном порядке переделываю. Есть пара идей…
В следующей статье попробую описать создание полноценного редактора на основе написанного кода. Возможно, даже с… но это секрет )

Использованные сайты и материалы:
-Спецификация JVM 7;
-Stackoverflow.com;
-Яндекс .

p.s.
1. Хоть я и старался делать всё аккуратно, в любую часть статьи могли закрасться ошибки. Как орфографические, так и смысловые. Код, который я написал, может показаться Вам медленным, глючным и вообще ужасным. Если Вы хотите, чтобы мир стал лучше, пожалуйста, сообщите мне об ошибках. В особенности, о логических. Рано или поздно, я натолкнусь на них, но за это время у многих людей может сложиться неправильное представление о вещах, описанных здесь.
2. Эта статья, в общем-то, является кратким пересказом части спецификации JVM с несколькими примерами кода. Несмотря не это, я считаю, что она может быть полезной, во-первых, для людей, начинающих изучение основ функционирования JVM, и, во-вторых, для расширения кругозора остальных людей. Ведь не у всех есть время на чтение многостраничных спецификаций. Мне, например, понравилась статья про хранение данных в файле flash'a, описанное в одной из статей, но самостоятельно я бы никогда, скорее всего, этого не узнал.
3. Удачи)

Автор: IDOL1234

Источник

Поделиться

* - обязательные к заполнению поля