Поэтому я пытаюсь изучить немного сборки, потому что мне это нужно для класса Computer Architecture. Я написал несколько программ, таких как печать последовательности Фибоначчи.
Я узнал, что всякий раз, когда я пишу программу, я использую эти 3 строки (как я узнал из сравнения кода сборки, сгенерированного из gcc
на него эквивалентно C
):
Я использую Manjaro 64 бит на виртуальной машине (4 ГБ оперативной памяти), процессор Intel 64 бит
Ответ 1
rbp
- это указатель на x86_64. В сгенерированном коде он получает моментальный снимок указателя стека (rsp
), так что при настройке rsp
(т.е. Резервировании пространства для локальных переменных или push
значений в стек) локальные переменные и параметры функции по-прежнему доступны из постоянное смещение от rbp
.
Многие компиляторы предлагают исключение указателя кадра как вариант оптимизации; это сделает сгенерированный код сборки доступным вместо переменных rsp
и освободит rbp
как другой регистр общего назначения для использования в функциях.
В случае GCC, который, как я предполагаю, вы используете из синтаксиса ассемблера AT & T, этот переключатель является -fomit-frame-pointer
. Попробуйте скомпилировать свой код с этим коммутатором и посмотрите, какой код сборки вы получите. Вы, вероятно, заметите, что при доступе к значениям относительно rsp
вместо rbp
смещение от указателя изменяется во всей функции.
Ответ 2
Linux использует архитектуру System V ABI для архитектуры x86-64 (AMD64); см. Систему V ABI в Wiki OSDev для деталей.
Это означает, что стек растет; меньшие адреса "выше" в стеке. Типичные функции C скомпилированы для
pushq %rbp ; Save address of previous stack frame
movq %rsp, %rbp ; Address of current stack frame
subq $16, %rsp ; Reserve 16 bytes for local variables
; ... function ...
movq %rbp, %rsp ; \ equivalent to the
popq %rbp ; / 'leave' instruction
ret
Объем памяти, зарезервированный для локальных переменных, всегда кратен 16 байтам, чтобы поддерживать стеки в 16 байт. Если для локальных переменных не требуется пространство стека, нет subq $16, %rsp
или аналогичной инструкции.
(Обратите внимание, что адрес возврата и предыдущий %rbp
в стек, равны 8 байтам, всего 16 байтов).
Пока %rbp
указывает на текущий стек стека, %rsp
указывает на вершину стека. Поскольку компилятор знает разницу между %rbp
и %rsp
в любой точке функции, он может использовать любой из них в качестве базы для локальных переменных.
Фрейм стека - это только локальная игровая площадка: область стека, используемая текущей функцией.
Текущие версии GCC отключают фрейм стека всякий раз, когда используются оптимизации. Это имеет смысл, потому что для программ, написанных на C, фреймы стека наиболее полезны для отладки, но не более того. (Вы можете использовать, например, -O2 -fno-omit-frame-pointer
чтобы сохранить фреймы стека, в то же время поддерживая оптимизацию.)
Хотя один и тот же ABI применяется ко всем бинарным файлам, независимо от того, на каком языке они записаны, некоторым другим языкам нужны фреймы стека для "разматывания" (например, для "исключения исключений" для вызывающей функции предка текущей функции); т.е. "раскручивать" стековые кадры, что одна или несколько функций могут быть прерваны, а управление передано некоторой функции предка, не оставляя ненужных вещей в стеке.
Когда кадры стека опущены - -fomit-frame-pointer
для GCC - реализация функции существенно меняется
subq $8, %rsp ; Re-align stack frame, and
; reserve memory for local variables
; ... function ...
addq $8, %rsp
ret
Поскольку нет фрейма стека (%rbp
используется для других целей, и его значение никогда не выталкивается в стек), каждый вызов функции выталкивает только обратный адрес в стек, который представляет собой 8-байтовое количество, поэтому нам нужно вычесть 8 из %rsp
чтобы он был кратным 16. (В общем, значение, вычитаемое из %rsp
и добавленное к %rsp
является нечетным кратным 8.)
Функциональные параметры обычно передаются в регистры. См. Ссылку ABI в начале этого ответа для деталей, но, короче говоря, интегральные типы и указатели передаются в регистры %rdi
, %rsi
, %rdx
, %rcx
, %r8
и %r9
, с аргументами с плавающей запятой в регистры %xmm0
до %xmm7
.
В некоторых случаях вы увидите rep ret
вместо rep
. Не путайте: rep ret
означает то же самое, что и ret
; префикс rep
, хотя обычно используется со строковыми инструкциями (повторные инструкции), ничего не делает при применении к команде ret
. Это просто, что некоторые предсказатели ветки процессоров AMD не любят прыгать в инструкцию ret
, и рекомендуемое обходное решение - вместо этого использовать rep ret
.
Наконец, я опустил красную зону над вершиной стека (128 байт по адресам меньше %rsp
). Это связано с тем, что он не очень полезен для типичных функций: в нормальном случае с корпусом-стоп-кадром вы хотите, чтобы ваши локальные файлы находились в фрейме стека, чтобы сделать отладку возможной. В случае с оцифрованным стеком требования к выравниванию стека уже означают, что нам нужно вычесть 8 из %rsp
, поэтому включая память, необходимую локальным переменным, в том, что вычитание ничего не стоит.