Все статьи / Исследуем работу компилятора C/C++


Есть всего три популярных, высококачественных, широко принятых в индустрии компиляторов C/C++:

  • GCC (Gnu Compiler Collections и GNU C Compiler), кроссплатформенный и Open-Source, используется в Linux как основной, на Windows известен как MinGW
  • MSVC (Microsoft Visual C/C++), низкая кроссплатформенность и закрытый код, используется в Windows как основной
  • LLVM/Clang, кроссплатформенный и Open-Source, используется в Mac OSX как основной, на Windows умеет быть совместимым и с MinGW, и с MSVC, доступен в Visual Studio 2015 и выше в модификации Clang/C2

Принципы работы GCC и Clang можно детально исследовать благодаря открытому исходному коду и отладочным средствам.

GCC (компиляция и вывод ассемблера)

Разберёмся, как использовать GCC из командной строки. На UNIX-платформах GCC доступен по команде gcc, а для Windows есть порт GCC — MinGW. Воспользуемся примером кода, складывающего два числа:

#include <stdio.h>

float sum(float a, float b)
{
    return a + b;
}

int main()
{
    float a = 0;
    float b = 0;
    scanf("%f %f", &a, &b);
    float ab = sum(a, b);
    printf("a + b = %f\n", ab);
}

Компиляция файла из командной строки с опциями по умолчанию (отладочная сборка без оптимизаций):

# после флага -o задан выходной путь
# все параметры вне флагов считаются входными путями
gcc a+b.c -o a+b

Вывод программы после запуска:

10 29
a + b = 39.000000

Получение ассемблерного кода для отладочного режима без оптимизаций возможно с опцией -S. По умолчанию создаваемый ассемблер использует синтаксис AT&T, который заметно отличается от синтаксиса Intel.

# -oa+b_debug.s необязательная опция, указывает явно имя выходного файла
# -S указывает генерировать ассемблер вместо исполяемого кода
gcc -S a+b.c -oa+b_debug.s
# -masm=intel указывает на смену синтаксиса выходного ассемблера
gcc -S a+b.c -oa+b_debug.s -masm=intel

Можно получить ассемблерный код в режиме с оптимизациями, используя флаг -O2, где “O” в верхнем регистре. Если сравнить отладочный и оптимизированный код с помощью утилиты diff, будут видны сильные отличия в цепочках инструкций.

# -oa+b_debug.s необязательная опция, указывает явно имя выходного файла
# -S указывает генерировать ассемблер вместо исполяемого кода
# -O2 указывает второй уровень оптимизаций, аналогичный Release-сборкам
gcc -O2 -S a+b.c -oa+b_debug.s

Вы можете скомпилировать ассемблер с помощью того же gcc, который сам передаст нужные параметры утилите “gas” (GNU Assembler).

gcc a+b_debug.s

Clang (компиляция, вывод ассемблера и LLVM-IR)

Clang разрабатывался как прозрачная замена компилятору GCC для Linux и Mac OSX. Поэтому большая часть опций, касающихся компиляции C/C++, у этих двух компиляторов совпадает. Компиляция примера на языке C выглядит точно так же:

# после флага -o задан выходной путь
# все параметры вне флагов считаются входными путями
clang a+b.c -o a+b

Генерация ассемблера с синтаксисом Intel:

clang -S -mllvm --x86-asm-syntax=intel a+b.c

Бекенды GCC и Clang

GCC и Clang оба используют гибкие фреймворки для построения бекендов компилятора. В GNU Compiler Collections используется собственный промежуточный язык и бекенд GIMPLE, который сильно упрощает написание компиляторов для новых языков в составе GNU Compiler Collections, но плохо подходит для изучения новичком. Проект LLVM гораздо дружественнее к новичкам и студентам, и именно его использует компилятор Clang.

Вы можете изучать промежуточный код проекта LLVM, называемый LLVM-IR, с помощью clang, исследуя преобразование кода из C в LLVM-IR:

# Выходной файл: a+b.ll
clang -S -emit-llvm a+b.c

# Компиляция с оптимизациями (O2)
# Выходной файл: a+b.ll
clang -O2 -S -emit-llvm a+b.c

Упражнения

  • Напишите 3-4 простейших программы в 10-20 строк на C (сложение двух чисел, вывод текущего времени с начала эпохи UNIX, вывод версии операционной системы, переворачивание строки т.п.). Сгенерируйте из этих программ листинги в машинном ассемблере либо в LLVM-IR, и сравните листинги от разных программ с помощью diff. Попробуйте собрать минимальный шаблон ассемблерного кода, который можно было бы разворачивать в полноценную программу путём подстановки цепочки инструкций вместо переменной {CODE}.