От машинного кода к языку: чтение кода и отображений
Суть Читай сниппеты assembly и высокоуровневого кода, прослеживай отображение исходник → machine code и отличай compilation от interpretation и JIT по тому, что и когда производит каждый.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min
Яснее всего различить слои — посмотреть на реальный код и спросить: сколько машинных инструкций из этого получится и когда происходит трансляция? Прочитай каждый сниппет и выбери ответ, который дал бы внимательный инженер.
Цель
Потренируй чтение отображения между слоями: assembly в machine code (один-к-одному), высокоуровневый оператор во много инструкций и разницу, которую вносят compile, interpret и JIT в то, что и когда производится.
На игрушечном CPU каждая инструкция занимает 2 байта. Сколько машинных инструкций и сколько байт assembler выпустит для этих четырёх строк и почему?
Heads-up Assembler не добавляет скрытых инструкций. Один-к-одному означает, что каждая строка — ровно одна машинная инструкция; строка ADD — это одна инструкция ADD, а операнды уже в R0 и R1 после LOAD'ов.
Heads-up Assembler не оптимизирует и не сворачивает инструкции — это работа compiler. Assembler подставляет mnemonic в опкод один-к-одному, поэтому четыре строки остаются четырьмя инструкциями.
Heads-up Число байт задаётся кодировкой инструкции, а не длиной текста mnemonic. Каждая инструкция здесь 2 байта, поэтому четыре инструкции — 8 байт.
Сниппет 2 — один высокоуровневый оператор
const total = price * quantity + shipping;
Викторина
Completed
Три переменные уже в памяти. Примерно во сколько машинных инструкций превращается эта одна строка TypeScript и кто решает, в какие?
Heads-up Строки исходника — это не машинные инструкции. Высокоуровневые операторы — многие-к-одному: это выражение становится загрузками, умножением, сложением и сохранением. Только assembly — один-к-одному.
Heads-up Программист не выбирал ни одного из них — в этом и абстракция. Compiler выбирает инструкции, распределяет регистры и разрешает адреса; ты написал лишь намерение.
Heads-up У CPU нет инструкции для произвольного выражения. Он исполняет отдельные загрузки, умножение, сложение и сохранение, которые выпустил compiler.
Сниппет 3 — три способа запустить одну программу
A) gcc add.c -o add && ./add # создаёт нативный бинарник, затем запускаетB) python3 add.py # interpreter читает и исполняет исходникC) node add.js # сперва интерпретация, затем JIT горячих функций
Викторина
Completed
Какое утверждение верно различает A, B и C по тому, что производится и когда?
Heads-up Только A пишет нативный бинарник заранее. Interpreter в B не производит machine code бинарник для твоей программы; C производит нативный код лишь во время работы, в памяти, для горячих функций.
Heads-up C — не чистый interpreter. Он сперва интерпретирует, затем JIT-компилирует часто исполняемые функции в нативный machine code во время работы, достигая скорости, близкой к скомпилированной, после прогрева.
Heads-up A компилирует один раз; ./add затем запускает готовый бинарник без накладных расходов на трансляцию. Это интерпретируемый путь B платит за трансляцию при каждом запуске и на каждом операторе.
Сниппет 4 — bytecode посередине
javac App.java -> App.class (bytecode, не нативный machine code)java App -> JVM загружает App.class и исполняет bytecode
Викторина
Completed
Что такое App.class и зачем доставлять bytecode вместо текста исходника или нативного бинарника?
Heads-up Bytecode — не нативный machine code, CPU не может исполнить его напрямую. JVM читает bytecode и интерпретирует или JIT-компилирует его в нативный код. Эта косвенность и делает один .class переносимым между CPU.
Heads-up Это не текст исходника; javac уже перевёл исходник в bytecode — компактный двоичный формат для JVM. Исходный текст Java — не то, что загружает JVM.
Heads-up Наоборот — bytecode не зависит от CPU. Любая машина с JVM запустит один и тот же .class; JVM наводит мост к нативному коду этой машины.
Итог
Чтение слоёв сводится к двум вопросам. Сколько машинных инструкций? Assembly — один-к-одному; высокоуровневый оператор — многие-к-одному, и инструкции выбирает compiler. Когда происходит трансляция и что производится? Compiler выпускает нативный бинарник заранее; чистый interpreter не выпускает ничего для твоей программы и транслирует каждый оператор во время работы; JIT сперва интерпретирует и компилирует горячие функции в нативный код на лету; а bytecode — переносимый промежуточный слой, который VM интерпретирует или JIT-компилирует. Сопоставь сниппет с механизмом — и остальное следует само.