ВПЕРЁД ⇒
⇐ НАЗАД
Функция 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
retONLINE COMPILER
Отличие от пустой функции 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 |
| Количество сгенерированных ассемблерных инструкций | 5 | 6 |
| Место на стеке под локальную переменную | не выделяется | выделяется 4 байта под [rbp-4] |
Добавление опции оптимизации -O2
Компилятор выбрасывает всё лишнее - оба варианта превращаются в одно и то же:
main:
xor eax, eax
retЗапись на стек исчезает, потому что эта ячейка никогда не читается - её существование не имеет наблюдаемых эффектов, и компилятор вправе её удалить согласно правилу as-if.
ВПЕРЁД ⇒
⇐ НАЗАД
Источники
- Здесь будет список источников
Категория
Теги
- Cpp Cpp
- Asm Asm
- Assembly Assembly
- Intel-asm-syntax Intel-asm-syntax
- x86-64 x86-64
- Стек Стек
- Stack Stack
- Стек-вызовов Стек-вызовов
- Call-stack Call-stack
- Стек-фрейм Стек-фрейм
- Stack-frame Stack-frame
- Указатель-фрейма Указатель-фрейма
- Frame-pointer Frame-pointer
- Указатель-стека Указатель-стека
- Stack-pointer Stack-pointer
- clang clang
- clang-fomit-frame-pointer clang-fomit-frame-pointer
- clang-O2 clang-O2