Как 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 и ассемблера.
Проверьте себя
1. Какой инструмент удобен для просмотра ассемблера, сгенерированного из C?
Agdb
Bgodbolt.org (Compiler Explorer)
Cnasm
Dmake
2. Что обычно делает оптимизация -O2 с простой функцией add?
AДобавляет больше команд для надёжности
BУбирает лишние обращения к стеку, оставляя минимум команд
CЗамедляет код ради читаемости
DПереводит код обратно в C
3. Почему одна строка C не всегда равна одной строке ассемблера?
AАссемблер не связан с C
BКомпилятор переставляет, склеивает и удаляет команды при оптимизации
CC нельзя компилировать
DЭто ошибка компилятора