💻 ПРОГРАММИРОВАНИЕ

Что на самом деле делает компилятор: путь от текста до машинного кода

Вы пишете осмысленные слова, а процессор понимает только числа. Между ними стоит компилятор — программа, которая разбирает ваш код на буквы, строит из него дерево и переписывает на язык железа.

Компилятор — это переводчик, который читает ваш текст один раз и навсегда переписывает его на язык, понятный процессору.
Процессор не знает слов if и while. Он знает только числа-команды. Вся работа компилятора — превратить ваши намерения в эти числа, ничего не перепутав.

Когда вы пишете программу, вы общаетесь не с компьютером, а с компилятором. Это посредник, который понимает и человеческий язык программирования, и язык железа, и берёт на себя перевод. Давайте проследим, что он делает, шаг за шагом, на примере одной строчки.

Шаг 1. Лексический анализ: режем текст на слова

Для компилятора исходный код — это просто длинная строка символов. Первое, что он делает, — разбивает её на токены: минимальные осмысленные кусочки. Строка x = 10 + y; превращается в последовательность: «имя x», «знак равно», «число 10», «плюс», «имя y», «точка с запятой».

Эту работу выполняет лексер (или сканер). Он похож на человека, который читает незнакомый текст и сначала просто выделяет отдельные слова, ещё не вникая в смысл фразы. Если вы написали 1abc — лексер уже здесь возмутится: число не может начинаться с цифры и продолжаться буквами.

Шаг 2. Синтаксический анализ: строим дерево

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

Выражение 10 + y * 2 превратится в дерево, где умножение окажется глубже сложения — потому что компилятор знает приоритет операций. Именно на этом этапе ловятся ошибки вроде забытой скобки или лишней точки с запятой: грамматика не сходится, дерево не строится.

Можно представить AST так:

    (=)
    / \
   x   (+)
       / \
      10  y

Шаг 3. Семантический анализ: проверяем смысл

Дерево синтаксически верное — но осмысленно ли оно? Здесь компилятор проверяет смысл: существует ли переменная y, можно ли складывать число со строкой, не присваиваете ли вы текст переменной для чисел. Он строит таблицу символов — список всех имён с их типами — и сверяется с ней.

Ошибки этого этапа знакомы каждому: «переменная не объявлена», «несовместимые типы». Синтаксис был чист, а смысл — нет.

Шаг 4. Оптимизация: делаем умнее

Прежде чем генерировать команды, хороший компилятор пытается улучшить программу, не меняя её поведения. Если вы написали 2 + 2, он посчитает это сам и подставит 4 — зачем тратить такты процессора в момент работы? Это называется сворачивание констант.

Он может выкинуть код, результат которого никому не нужен, вынести из цикла вычисление, которое не меняется на каждой итерации, и сделать ещё десятки трюков. Поэтому одна и та же программа, скомпилированная «без оптимизаций» и «с оптимизациями», работает с заметно разной скоростью.

Шаг 5. Генерация кода: говорим с железом

Финал. Из оптимизированного дерева компилятор порождает машинный код — те самые числа-команды для конкретного процессора. Сложение в исходнике превращается в инструкцию вроде ADD, обращение к переменной — в чтение по адресу памяти.

Результат складывается в исполняемый файл. Теперь он не нуждается ни в компиляторе, ни в исходном коде: процессор берёт его и выполняет напрямую.

А как же Python и JavaScript?

Здесь живёт частое заблуждение. В таких языках первые шаги (лексер, парсер, AST) выглядят почти так же, но дальше код не превращают в файл-для-железа. Его выполняет интерпретатор — программа, которая обходит дерево или промежуточный байт-код и делает то, что в нём написано, прямо на лету. Поэтому Python-скрипт можно запустить сразу, но в среднем он медленнее: перевод происходит во время работы, а не заранее.

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

Зачем это знать

Понимая этапы, вы перестаёте бояться сообщений об ошибках. «Unexpected token» — это споткнулся парсер. «Undefined variable» — недоволен семантический анализ. А если программа работает быстрее с одним флагом компиляции и медленнее с другим — вы знаете, что на сцену вышла оптимизация. Компилятор перестаёт быть чёрным ящиком и становится понятным конвейером.

#как устроено#компилятор#машинный код#программирование