Как C компилируется в ассемблер
Урок связывает всё вместе: показывает, во что компилятор превращает обычную функцию на C.
Компилятор переводит код высокого уровня в ассемблер, попутно распределяя переменные по регистрам и оптимизируя вычисления.
Зачем читать вывод компилятора
Понимание того, во что превращается ваш C-код, помогает писать быстрее и отлаживать трудные баги. Удобнее всего смотреть это на сайте godbolt.org (Compiler Explorer): пишете C слева — видите ассемблер справа, строки подсвечены соответствиями.
Простая функция и её ассемблер
Возьмём функцию сложения на C:
int add(int a, int b) {
return a + b;
}Без оптимизаций компилятор GCC выдаёт примерно такой ассемблер (NASM-подобно):
add:
push rbp ; пролог
mov rbp, rsp
mov [rbp-4], edi ; сохранить a (1-й арг)
mov [rbp-8], esi ; сохранить b (2-й арг)
mov eax, [rbp-4] ; eax = a
add eax, [rbp-8] ; eax = a + b
pop rbp ; эпилог
ret ; вернуть eaxВы узнаёте здесь всё из курса: пролог/эпилог, аргументы в edi/esi по System V, результат в eax.
Сила оптимизаций
С флагом -O2 компилятор выкидывает лишнее обращение к стеку и оставляет почти ничего:
add:
lea eax, [rdi + rsi] ; eax = a + b одной командой
retКомпилятор понял, что хранить аргументы в памяти не нужно, и сложил их прямо через lea. Вот почему ассемблер от современного компилятора обычно лучше написанного руками.
Как работает под капотом
Промоделируем, что делает -O2-версия: складываем два аргумента и возвращаем — без всякого стека.
def add_opt(rdi, rsi): # аргументы в rdi, rsi
eax = rdi + rsi # lea eax, [rdi + rsi]
return eax # ret -> результат в eax
print("add(2, 40) =", add_opt(2, 40))Вывод:
add(2, 40) = 42
Оптимизированный код делает ровно то же, что наивный, но за две команды вместо восьми. На godbolt можно сравнить версии -O0 и -O2 и буквально увидеть, как исчезают лишние шаги.
Частые ошибки
- Судить о коде по неоптимизированной версии. Реальные сборки идут с
-O2;-O0намеренно «многословен» для отладки. - Ждать строку-в-строку соответствия. Компилятор переставляет и склеивает команды, поэтому одна строка C — это часто несколько строк ассемблера или ноль.
- Думать, что переменные всегда в памяти. Оптимизатор держит их в регистрах, пока хватает места.
Итог
- Компилятор переводит C в ассемблер и распределяет переменные по регистрам.
- В сгенерированном коде видны пролог/эпилог и System V из этого курса.
- Оптимизации (
-O2) убирают лишние обращения к стеку и команды. - godbolt.org — лучший инструмент посмотреть соответствие C и ассемблера.