ВПЕРЁД

НАЗАД



Функция main() с return 0

Код

C++

int main() {
	return 0;
}

ASM (x86-64 clang)

main:
	push rbp
	mov rbp, rsp
	mov dword ptr [rbp - 4], 0
	xor eax, eax
	pop rbp
	ret

ONLINE COMPILER

Godbolt

Отличие от пустой функции main()

Единственная новая инструкция здесь - mov dword ptr [rbp - 4], 0. Всё остальное идентично предыдущему примеру. Интересно то, что итоговый результат тот же - возврат нуля, но компилятор делает это немного иначе.

Новая инструкция

mov dword ptr [rbp - 4], 0

Это явный return 0 из исходного кода:

rbp - 4 - адрес на стеке, 4 байта ниже указателя фрейма. Компилятор резервирует место на стеке под временное хранение возвращаемого значения, как если бы существовала скрытая локальная переменная int __retval.

dword ptr - указание размера операнда: double word = 32 бита = 4 байта, что соответствует размеру int на x86-64.

[...] - квадратные скобки означают обращение по адресу (разыменование), а не к самому регистру. То есть значение 0 записывается в память по этому адресу, а не в регистр.

В итоге эта инструкция буквально записывает 0 в зарезервированную ячейку стека.

Верхние адреса памяти
----------------------------------------------------------------
                            <  main ret addr  >
    rsp → (новый rbp)     → <    старый rbp   >
          (новый rbp - 4) → < hex 00 00 00 00 > ← сюда пишет mov
----------------------------------------------------------------
Нижние адреса памяти

Почему тогда снова xor eax, eax?

Можно ожидать, что после записи 0 на стек компилятор сделает что-то вроде mov eax, [rbp - 4] - прочитает значение обратно в регистр. Но вместо этого снова стоит xor eax, eax.

Компилятор знает, что только что записал 0, поэтому читать обратно бессмысленно - можно сразу занулить eax напрямую. Это простейшая оптимизация, даже на уровне -O0 (уровень без оптимизации).

Иначе говоря, mov dword ptr [rbp - 4], 0 - это честное отражение исходного кода (явный return 0) для возможности получения информации о стек-фрейме в процессе отладки, а xor eax, eax - это уже работа компилятора по подготовке реального возврата через регистр.

Сравнение двух примеров

int main() {}int main() { return 0; }
Способ возврата из main()неявный return 0явный return 0
Количество сгенерированных ассемблерных инструкций56
Место на стеке под локальную переменнуюне выделяетсявыделяется 4 байта под [rbp-4]

Добавление опции оптимизации -O2

Компилятор выбрасывает всё лишнее - оба варианта превращаются в одно и то же:

main:
    xor eax, eax
    ret

Запись на стек исчезает, потому что эта ячейка никогда не читается - её существование не имеет наблюдаемых эффектов, и компилятор вправе её удалить согласно правилу as-if.

Godbolt



ВПЕРЁД

НАЗАД