Как выглядит байт код
Перейти к содержимому

Как выглядит байт код

  • автор:

Как посмотреть байт код java

Для просмотра байт-кода Java можно использовать утилиту javap , которая поставляется вместе с JDK . javap позволяет просмотреть байт-код любого класса, даже если он не содержит исходного кода.

Чтобы просмотреть байт-код класса, следует выполнить следующие шаги:

  • Скомпилировать Java-класс в .class файл. Это можно сделать, например, с помощью javac команды:
javac MyClass.java 
  • Запустить javap с флагом -c , чтобы вывести байт-код класса:
javap -c MyClass 

Эта команда выведет байт-код класса MyClass.

Compiled from "MyClass.java" public class MyClass  public MyClass(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public void myMethod(); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello, World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return > 

Этот вывод показывает байт-код двух методов класса MyClass : конструктора и метода myMethod()

Понимаем байт-код EVM: Часть 2

В первой части мы изучили часть создания контракта в байт-коде виртуальной машины. В этом разделе мы проанализируем рантайм байт-кода виртуальной машины. Мы по-прежнему будем использовать пример кода из предыдущей статьи.

Весь байткод выглядит так:

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

Эти инструкции фактически сохраняют адрес 0x80 со смещением 0x40 в памяти в качестве указателя свободной памяти для использования в будущем.

Существует новый код операции CALLDATASIZE, который мы раньше не встречали в 0x07. Он получит размер полезной нагрузки данных из этой транзакции. LT — это код операции для сравнения двух элементов в стеке, он вернет значение TRUE, если сравнение выполнено.

Итак, собрав все части вместе, мы можем получить эквивалентный ассемблерный код Solidity следующим образом:

mstore(0x40,0x80);

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

Это 4-байтовое значение будет использоваться контрактом для выбора функции для доставки остальных данных, которые являются параметрами для этой функции. Например, если вы вызовете функцию withdraw(0xABCD) в смарт-контракте, полезная нагрузка данных для этого вызова будет выглядеть следующим образом:

0x3823D66C000000000000000000000000000000000000000000000000000000000000ABCD

В этом примере первое 4-байтовое значение равно 0x3823D66C, что является хэш-значением SHA3 для “withdrawn(bytes32)”. Следующее 32-байтовое целое число является параметром вызова функции 0xABCD. Это простой пример параметров целочисленной функции. При обработке параметров переменного размера все усложнится. Мы поговорим о них позже.

А пока давайте вернемся к инструкциям, которые мы обсуждали. Размер полезной нагрузки данных должен составлять не менее 4 байт. Если нет, то все вернется на круги своя. Но будет ли это справедливо для всех смарт-контрактов? Что, если мы просто отправим эфир в этот смарт-контракт без вызова какой-либо функции?

Возможно, вы уже помните некоторые функциональные возможности в программах Solidity. Да, именно там реализована функция fallback. Чтобы подтвердить это, вы можете получить образец с реализованной функцией fallback, а затем проверить ветвь инструкции после кода проверки msg.data.length.

Продолжая разбор кода, мы видим следующие команды:

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

Сначала код помещает 0xFFFFFFFF в стек. Это значение будет использоваться в качестве одного из операндов кода операции AND в 0x33. Затем код добавит еще одно огромное постоянное значение, которое будет использоваться в качестве разделителя DIV в 0x32. Для инструкций в 0x2F, 0x31 код получит первое 32-байтовое значение из полезной нагрузки данных, используя CALLDATALOAD(0x0).

В сочетании с набором команд более поздних DIV и AND мы можем видеть, что эти инструкции фактически получают первое 4-байтовое значение полезной нагрузки данных, которое является хэш-значением подписи функции. Вычисленный результат помещается в стек. Затем значение сравнивается с 0x1003e2d2, используя код операции EQ в 0x3A. Если это было правдой, выполнение будет переведено на адрес 0x4D с помощью JUMPI. В противном случае код продолжит работу, и результат будет сравнен с другим хэш-значением 0xb69ef8a8.

Теперь логика этого фрагмента кода довольно ясна. Он получает первое 4-байтовое значение из полезной нагрузки данных и решает, какая функция будет выбрана для запуска. Для кодовых адресов 0x4D и 0x74 они являются входом для каждой public функции, к которой вызывающий может получить доступ к смарт-контрактам. Если ни одно из хэш-значений в коде не удовлетворяется, то это приведет к срабатыванию fallback функции смарт-контрактов. Если он не был определен, он просто вернется.

До сих пор мы знали, как public функции могут получить доступ к внешним функциям. Теперь давайте подробнее рассмотрим конкретные функции. Адрес 0x4D является входом функции add():

На входе в функцию add() находится код операции JUMPDEST. Это специальный код операции, который помечает только адрес, на который можно перейти. Похоже, что это не играет важной роли для реализации EVM. Однако вы увидите, что это действительно помогает определить график потока управления (CFG) для байт-кода. Мы обсудим это в следующем разделе.

Инструкции, установленные в 0x4F-0x57, были просмотрены в предыдущем разделе. Они были введены компилятором для функции, не принимающей средства. После этого кода проверки код операции PUSH1 по адресу 0x5A легко проигнорировать. Однако это PUSH1 очень важно для того, чтобы код мог вернуться позже.

Давайте просто запомним, что адрес 0x62 пока помещен в стек. Затем вызывается CALLDATALOAD(0x04) для загрузки параметра из полезной нагрузки данных, который расположен со смещением 0x04. После получения параметра код перейдет к 0x86 для выполнения:

В приведенном выше фрагменте кода значение 0x0 в хранилище загружается с помощью SLOAD(0x0). Затем это значение будет добавлено к параметру, загруженному из полезной нагрузки данных, и сохранено обратно в то же место 0x0 в хранилище. Наконец, мы видим код, который мы поместили внутри функции add():

balance = balance + value;

Мы использовали только одну целочисленную переменную balance внутри смарт-контракта. Компилятор присваивает этой переменной смещение 0x0. Таким образом, любая операция чтения или записи с этой переменной баланса будет помещена в смещение 0x0 в хранилище.

В конце фрагмента кода используется JUMP для возврата к 0x62. Если вы все еще помните, где это значение 0x62 было помещено в стек. Эта операция может напомнить вам что-то об архитектуре X86. Да, это на самом деле call и ret для вызовов функций. Поскольку EVM не поддерживает вызовы функций на уровне байт-кода, он может использовать только коды операций PUSH и JUMP для вызовов функций. Таким образом, возникнет много проблем с созданием CFG из байт-кода.

Давайте вернемся к адресу 0x62, чтобы посмотреть, что произойдет дальше:

Этот фрагмент кода содержит несколько настроек элементов в стеке с помощью кодов операций DUP и SWAP. Для эквивалентного кода на ассемблере приведенный выше код выглядит следующим образом:

mstore(mload(0x40), value);

return(mload(0x40), 0x20);

Value внутри кода было вычисленным результатом предыдущей операции add(), которое является новым значением balance. Наконец, мы просмотрели весь байт-код внутри функции add().

Теперь давайте вернемся к фрагменту кода отправки функции для второй функции HASH 0xb69ef8a8. Вход для этой функции находится в 0x74:

Мы видим, что первая часть кода действительно похожа на предыдущую функцию, за исключением того, что параметр не был загружен. Затем функция вызовет фрагмент кода с 0x95. Инструкции в 0x95-0x98 просто загружают значение в 0x0 в хранилище и возвращают. Мы отметили, что фрагмент кода в 0x62 был повторно использован для обеих функций. Это связано с тем, что обе функции вернут переменную balance обратно.

Вы можете задаться вопросом, почему внутри кода отправки функции присутствует функция HASH 0xb69ef8a8? Разве внутри смарт-контракта нет только одной функции add()? Если вы используете базу данных в 4 байта для проверки этого хэша, вы получите balance(). По-видимому, переменная хранилища распознается компилятором как public функция без параметров.

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

Как это будет выглядеть для сопоставлений или массивов переменной длины? Как будут представлены параметры в полезной нагрузке данных для строк? Мы поговорим обо всем этом в следующем разделе.

Как объяснить, что такое байткод? [закрыт]

Закрыт. На этот вопрос невозможно дать объективный ответ. Ответы на него в данный момент не принимаются.

Хотите улучшить этот вопрос? Переформулируйте вопрос так, чтобы на него можно было дать ответ, основанный на фактах и цитатах.

Закрыт 3 года назад .

Пытался объяснить начинающим инженерам, что такое байткод (в рамках пояснения принципов выполнения Java программ). Так вот, я пытаюсь стандартно объяснить, мол это что то между компиляцией и интерпретацией, типа каждая команда джавовского байт кода, это как несколько ассемблеровских комманд, но они смотрят на меня и говорят — не понятноооо! Они не программисты, не знают что такое ни компиляция, ни интерпретация, ни ассемблер, это я попытался на пальцах им объяснить, и вроде до них что-то дошло, а как можно на пальцах используя жизненные примеры объяснит принцип работы байткода?

Отслеживать
23.4k 3 3 золотых знака 50 50 серебряных знаков 70 70 бронзовых знаков
задан 18 мая 2013 в 20:37
vanyamelikov vanyamelikov
3,113 3 3 золотых знака 30 30 серебряных знаков 59 59 бронзовых знаков
Если они не программисты, то им это не нужно. Не тратьте время.
18 мая 2013 в 20:44
18 мая 2013 в 20:45

Я сам не программист(по образованию), сейчас стоит вопрос проведения расчета охлаждения камеры сгорания ракетного двигателя. Так вот я хочу дать троим студентам эту задачу, на трех разных языках, и посмотреть у кого лучше получиться. Языки: java-среда эклипс, Си++ — Visual Studio и Visual Basic да еще и 6. Сам я его накидал в MathCAD и MathLab. Думаю должен получиться хороший эксперимент!

18 мая 2013 в 20:51

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

19 мая 2013 в 6:05

Хотел было уже написать, но заглянул в Википедию и понял, что ничего добавить не смогу. @vanyamelikov, а Ваши коллеги эту статью читали?

19 мая 2013 в 19:50

7 ответов 7

Сортировка: Сброс на вариант по умолчанию

А нужно объяснять очень просто. Используя их положение. Для начала показываем этим инженерам программу на Java. Например, классический HelloWorld. И спрашиваем — понятно ли? Скорее всего они скажут нет. Объясняем, что и специальной программе, которая исполняет жава код, тоже не понятно. Для этого нужно «разобрать по косточкам».

Теперь делаем «псевдотрасляцию» — как для машинистки (секретарши). Для HelloWorld’а она будет такая.

  • настроить окружение (в коде этого нет, но это автоматом) — приготовить бумагу, проверить катриджи.
  • взять из памяти строку «привет мир».
  • нижимая кнопки, побуквенно ввести сроку (здесь появился цикл:) ).
  • почистить все за собой и отнести бумагу заказчику.

Формально — это и есть простой байткод. Только это человеческий байткод. А если в нем стандартизировать все операции и занумеровать, то все может быть сведено к набору чисел. Теперь к реальному байткоду перейти просто.

Легко будет объяснить и переносимость. Если человек (секретарша) выучит все коды операций, то она сможет выполнить любую работу, главное, что бы была последовательность кодов. А инженеры могут попробовать спаять-сконструировать устройство, которое будет это исполнять.

Машинный код и байт код: на каком языке говорит ваша программа?

У тех, кто только начинает знакомиться с Java, довольно часто возникает путаница в понятиях машинный и байт код. Что они собой представляют? В чём различия? В короткой заметке мы постараемся максимально просто и понятно расписать их особенности, чтоб раз и навсегда закрыть этот вопрос.

Машинный код и байт код: на каком языке говорит ваша программа? - 1

Машинный код

Процессор — это, по сути, очень сложный и продвинутый калькулятор. У него есть множество ячеек памяти (называемых регистрами) с которыми и между которыми проводятся различные математические и байтовые операции. Машинный код как раз и представляет собой описание последовательности выполнения операций и набора участвующих данных. По сути, это единственный язык, который понимает процессор вашего компьютера.

Врожденная несовместимость

При этом далеко не все процессоры «говорят» на одном языке. Различия есть не только между архитектурами CISC и RISC, но и внутри этих «лагерей».

CISC (англ. Complex Instruction Set Computing) — концепция проектирования процессоров, которая характеризуется следующим набором свойств:

  • много команд, разных по длине;
  • много режимов адресации;
  • сложная кодировка инструкции.

В новых поколениях процессоров внедряют дополнительные наборы инструкций, которые моделям старшего поколения попросту неизвестны. Из-за этого программы, скомпилированные для одной архитектуры (или одного поколения процессоров) не могут работать на другом аппаратном обеспечении. Все это вынуждает заниматься перекомпиляцией программ для обеспечения их работы на других компьютерах. Впрочем, заново компилировать приходится не только из-за процессоров, но и еще из-за различий во взаимодействии программ и операционной системы. Именно из-за них невозможно запустить «виндовую» программу под Linux, а «линуксовую» под Windows.

Байт-код

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

JIT-компиляция (англ. Just-in-time compilation, компиляция «на лету») или динамическая компиляция (англ. dynamic translation) — это технология увеличения производительности программных систем, использующих байт-код, путём компиляции байт-кода в машинный код или в другой формат непосредственно во время работы программы. «Официально» в Java до 9-й версии был только JIT-компилятор. В Java 9 появился ещё один компилятор, причём компилирует он с опережением (AoT). Эта возможность позволяет компилировать классы Java в нативный код перед запуском на виртуальной машине. Данная функция предназначена для улучшения времени запуска и малых, и больших приложений, с ограниченным влиянием на максимальную производительность.

Для CISC процессоров некоторые инструкции могут объединяться в более сложные конструкции, поддерживаемые процессором, а для RISC – наоборот разбиваться на более простые последовательности команд.

Еще и виртуальная ОС

Впрочем, байт код содержит не только процессорные инструкции. В нем также содержится логика взаимодействия с виртуальной операционной системой, которая делает поведение приложения независящим от используемой на компьютере операционной системы. Это отлично видно в JVM, где работа с системными вызовами и GUI зачастую не зависят от ОС, на которой запущена программа. По большому счету JVM эмулирует запуск процесса программы, в отличие от решений вроде Virtual Box, которые создают только виртуальную систему/железо.

JVM одна такая?

Определенно нет. Тот же DotNet CLI это тоже виртуальная машина, которую чаще всего используют на компьютерах, работающих под Windows с x86 совместимыми процессорами. Впрочем существует ее реализация и под другие системы: приложения под него должны работать в Windows RT запущенной на ARM (RISC) совместимых процессорах, или можно запустить их на Linux/OSX в среде Mono, являющей сторонней (и потому не полностью совместимой) реализацией DotNet для этих платформ. Так что эта платформа, как и JVM, работает на разных процессорах и разных ОС. Существует еще множество похожих решений (как старых, так и новых): LLVM, Flash SWF, и другие. У некоторых языков программирования есть собственные виртуальные машины. К примеру, CPython компилирует исходники из PY в файлы PYC – скомпилированный (compiled) байт код который подготовлен к запуску в PVM. Или есть намного более древний пример — Lisp можно компилировать в файлы FASL (Fast Load). Фактически они содержат AST дерево, построенное генератором из исходного кода. Эти файлы могут быть прочитаны и запущены интерпретатором Lisp на разных платформах, или использованы для создания машинного кода для используемой на данный момент аппаратной архитектуры.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *