Добавить в корзинуПозвонить
Найти в Дзене

Препроцессинг и компиляция: от текста до промежуточного представления

Clang и LLVM IR: на каком языке думают компиляторы. Первый этап - препроцессинг. Это самая простая часть: компилятор берёт ваш main.c и выполняет текстовые директивы. Он выкидывает коментарии, вставляет заголовочные файлы (#include), раскрывает макросы (#define) и обрабатываеи условную компиляцию (#ifdef). В Clang остановиться после препроцессора можно флагом -E: clang -E main.c -o main.i Теперь у нас есть один большой .i-файл с чистым кодом на C. На этом этапе часто кроются проблемы с макросами или неверными путями к заголовочным файлам. Следующий шаг - собственно компиляция. Здесь Clang делает синтаксический и семантический анализ. Ошибки вроде забытой точки с запятой или несоответствия типов отлавливаются именно сейчас. Но Clang идёт дальше: вместо того чтобы сразу генерировать ассемблер, он создаёт промежуточное представление LLVM IR (Intermediate Representation). LLVM IR - это нечто среднее между C и ассемблером, но не привязанное к конкретному процессору. Его можно представит

Препроцессинг и компиляция: от текста до промежуточного представления

Clang и LLVM IR: на каком языке думают компиляторы.

Первый этап - препроцессинг. Это самая простая часть: компилятор берёт ваш main.c и выполняет текстовые директивы. Он выкидывает коментарии, вставляет заголовочные файлы (#include), раскрывает макросы (#define) и обрабатываеи условную компиляцию (#ifdef). В Clang остановиться после препроцессора можно флагом -E:

clang -E main.c -o main.i

Теперь у нас есть один большой .i-файл с чистым кодом на C. На этом этапе часто кроются проблемы с макросами или неверными путями к заголовочным файлам.

Следующий шаг - собственно компиляция. Здесь Clang делает синтаксический и семантический анализ. Ошибки вроде забытой точки с запятой или несоответствия типов отлавливаются именно сейчас. Но Clang идёт дальше: вместо того чтобы сразу генерировать ассемблер, он создаёт промежуточное представление LLVM IR (Intermediate Representation).

LLVM IR - это нечто среднее между C и ассемблером, но не привязанное к конкретному процессору. Его можно представить как код для виртуальной машины с бесконечным числом регистров. Вот как выглдит простейшая функция main в IR:

define i32 @main() {

ret i32 0

}

Этот код уже оптимизирован, но ещё не превращён в машинные инструкции. Посмотреть его можно командой:

clang -S -emit-llvm main.c -o main.ll

Зачем это нужно? Потому что на уровне IR работают мощные оптимизаторы. LLVM может применить оптимизации, не думая о конкретной архитектуре: удалить мёртвый код, заменить константы, развернуть циклы. Причём оптимизации можно отложить до этапа компоновки (Link-Time Optimization, LTO), когда видна вся программа целиком.

После оптимизаций бэкенд LLVM транслиурет IR в ассемблер целевого процессру. Для этого ему нужно знать архитектуру: ARM, AVR, RISC-V. Это задаётся флагами вроде -target armv7m-none-eabi -mcpu=cortex-m3. Результат - файл .s с ассемблерными инструкциями.

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

#компиляция #LLVM #Clang #IR #оптимизация #микроконтроллеры #программирование