JVM exceptions are weird: a decompiler perspective
Исследуя декомпиляцию Java-классов, автор обнаружил, что обработка исключений в JVM значительно сложнее обычного потока управления. В отличие от явных конструкций вроде if и while, которые реализуются через инструкции перехода, исключения обрабатываются неявно через отдельную "таблицу исключений". Эта таблица связывает регионы байткода с соответствующими обработчиками, очищает стек при исключении и передает управление обработчику.
Особую сложность представляют крайние случаи: регионы в таблице могут пересекаться без вложенности, а целевые обработчики могут находиться перед началом региона или даже внутри него. Реальные Java-файлы часто нарушают интуитивные предположения о структуре, что усложняет декомпиляцию. Особенно проблематичны конструкции try-finally, где finally-блок должен выполняться независимо от возникновения исключения, но должен знать, куда передать управление после завершения.
Комментарии (4)
- Основной фокус обсуждения - сравнение исключений в данном рантайме с другими механизмами управления потоком.
- Уточнение, что сравнение проводится не с исключениями в других рантаймах.
- Позитивная оценка стиля и качества написания поста автором.
- Благодарность автора за публикацию поста.
JVM exceptions are weird: a decompiler perspective
Автор исследует сложность обработки исключений JVM при декомпиляции Java-байткода. Изначально предполагалось, что метод декомпиляции управляющих потоков, разработанный автором, можно легко расширить для обработки исключений, но оказалось, что множество крайних случаев значительно усложняют задачу. JVM использует стековую архитектуру, где обычные управляющие конструкции реализуются явно, а исключения обрабатываются неявно через отдельную таблицу исключений. Эта таблица связывает регионы инструкций с обработчиками, но JVM не требует соблюдения иерархической вложенности обработчиков, что приводит к пересекающимся диапазонам и нарушает интуитивные ожидания.
Реальные Java-файлы часто содержат такие "неестественные" конструкции, что делает проблему важной для любого декомпилятора. Автор также отмечает сложность обработки try...finally блоков, где код finally должен выполняться независимо от возникновения исключения, но передача управления после его завершения зависит от контекста. Эти особенности байткода и компилятора javac создают значительные препятствия для создания корректного декомпилятора, особенно при попытке восстановления исходной структуры кода.
Комментарии (46)
- Обсуждение началось с примера кода, который выглядит как баг, но на самом деле является корректным поведением в Java и других языках, где finally блок не может перехватить return/throw, но может перехватить continue/break.
- Участники обсудили, что в действительности это не баг, а фича, и что в большинстве языков finally не может перехватить return/throw, но может перехватить continue/break.
- Обсуждались также вопросы, что в некоторых языках, таких как C#, такие конструкции вообще не допускаются.
- Также было отмечено, что в Java 25 такой код будет компилироваться и работать, но в более ранних версиях Java это вызовет ошибку компиляции.