Главная / ПрограммированиеСоздаём язык программирования → Лекция П1. Виртуальные машины: они повсюду

Ассемблер и машинные инструкции

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

Ассемблерные инструкции работают с такими вещами, как “регистры” (индивидуальные ячейки памяти сверхбыстрого доступа), “стек” (участок памяти, функционирующий по принципу «первый зашёл, первый вышел»); могут в одну инструкцию произвести “умножение”, “сложение”, или “условный переход к другой области памяти”; и т. д.:

; пример пары ассемблерных комманд
MOV 1, A ; закинуть число 1 в регистр A
INC A    ; увеличить A на 1 (теперь там 2)

За последнее время вышло множество разных игр, популяризирующих ассемблер (разработанных компанией Zachtronics). Интересующимся рекомендуется попробовать в них поиграть — довольно увлекательно и познавательно.

А транзисторы — это совсем другой уровень. На одном крае шкалы это динамика p-n переходов, изменение электрических потенциалов и т. д.; физика, а не лирика. А на другом — это целые города, мегаполисы, дата-центры вычислительных элементов и сетей.

Chip cities

Силиконовая пластина в процессе изготовления.

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

Отсюда можно сделать вывод, что набор инструкций x86 (или x86-64), который считается самым низким уровнем разработки (уровнем загрузчиков операционных систем и драйверов), максимально близким к железу, по сути является набором инструкций для некоей абстракции.

Для «виртуальной машины», эмулируемой микрокодом процессора поверх «реальных транзисторов».

Чуть более высокий уровень абстракции — известный программный комплекс LLVM. LLVM представляет собой, в сердцевине, некий воображаемый «обобщённый автомат» (в первую очередь, «обобщённый процессор») с «универсальными машинными инструкциями».

Код «машинных инструкций» LLVM путём ряда преобразований может быть переведён в машинные коды процессоров различных архитектур (набор инструкций разных процессоров сильно различается): например, x86 (настольные компьютеры), ARM (мобильные устройства), различные микроконтроллеры.

Получается, один и тот же низкоуровневый код может быть сравнительно безболезненно перенесён на различные архитектуры.

Традиционные виртуальные машины

Такие среды разработки и исполнения кода, как Java, .NET, Erlang предоставляют свои варианты виртуальных машин. Каждая со своим набором «базовых инструкций» и своими «машинными кодами» (называемым «байт-кодом»).

Изначальная мотивация для придумывания «виртуальных машин» поверх набора инструкций реальных процессоров, пожалуй, двоякая:

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

Во всех трёх перечиселнных примерах виртуальных машин (Java, .NET, Erlang BEAM) используется так называемая автоматическая сборка мусора — принцип управления памятью, при которой программисту не надо вручную отмечать более не используемые участки памяти (на уровне переменных, ссылок на объекты и т. п.), как приходится делать при программировании в парадигме «машины x86» (или любой другой низкоуровневой процессорной архитектуры).

Как правило, виртуальная машина (в отличие от LLVM, описанной выше, которая в строгом смысле виртуальной машиной не является) транслирует собственный байт-код в машинные инструкции платформы времени выполнения «по ходу дела», прямо во время выполнения программы.

Компиляторы и интерпретаторы

Традиционно языки программирования разделяют на «компилируемые» и «интерпретируемые».

В компилируемых языках исходные коды транслируются в некое промежуточное представление, которое сохраняется на жёсткий диск. Например, в набор машинных кодов. Или в байт-код некоей виртуальной машины, которая будет их выполнять.

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