x86_64 ABI: проблема с разборкой
У меня есть следующий код C:
#include <stdio.h>
int function(int a, int b)
{
int res = a + b;
return res;
}
int main(){
function(1,2);
exit(0);
}
Я компилирую его для x86-64 с помощью gcc 4.8.2 (под Ubuntu 14), и он выдает следующий код:
000000000040052d <function>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 89 7d ec mov %edi,-0x14(%rbp)
400534: 89 75 e8 mov %esi,-0x18(%rbp)
400537: 8b 45 e8 mov -0x18(%rbp),%eax
40053a: 8b 55 ec mov -0x14(%rbp),%edx
40053d: 01 d0 add %edx,%eax
40053f: 89 45 fc mov %eax,-0x4(%rbp)
400542: 8b 45 fc mov -0x4(%rbp),%eax
400545: 5d pop %rbp
400546: c3 retq
Я не могу понять некоторые вещи.
Сначала мы нажимаем rbp и сохраняем rsp в rbp. Затем на вершине стека (и при % rbp) мы получили сохраненный rbp. Тогда все, что ниже rbp - это свободное пространство.
Но затем мы помещаем переданные параметры из edi и esi в -0x14(%rbp) и ниже.
Но почему мы не можем поместить их сразу ниже того, на что указывает rbp / rsp? edi и esi имеют длину 4 байта, тогда почему не -0x8(%rbp) и -0xc(%rbp)? Это связано с выравниванием памяти?
И почему существует странное сохранение eax в стеке и чтение его перед возвратом?
1 ответ
Прежде всего, обратите внимание, что вы смотрите на неоптимизированный вывод компилятора. Выходные данные компилятора часто оказываются глупыми с отключенными оптимизациями, потому что компилятор буквально переводит каждую строку C в эквивалентный прогон сборки, не заботясь даже о самых простых и очевидных оптимизациях.
На ваш первый вопрос ответ "потому что именно там ваш компилятор решил, что переменные должны идти". Нет лучшего ответа - компиляторы сильно различаются по своим схемам размещения стеков. Например, Clang на моей машине выводит это вместо:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl -8(%rbp), %esi
movl %esi, -12(%rbp)
movl -12(%rbp), %eax
popq %rbp
retq
где вы можете ясно видеть, что a
хранится в -4, b
хранится в -8, и result
хранится в -12. Это более плотная упаковка, чем то, что дает вам ваш GCC, но это всего лишь причуда GCC и не более того.
Что касается вашего второго вопроса, давайте просто посмотрим, как инструкции отображаются на C:
Стандартный пролог функции (настройка фрейма стека):
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
Сохранить два аргумента в переменных стека a
а также b
:
400531: 89 7d ec mov %edi,-0x14(%rbp)
400534: 89 75 e8 mov %esi,-0x18(%rbp)
нагрузка a
а также b
за a + b
400537: 8b 45 e8 mov -0x18(%rbp),%eax
40053a: 8b 55 ec mov -0x14(%rbp),%edx
На самом деле сделать a + b
40053d: 01 d0 add %edx,%eax
Задавать result = (result of a+b)
40053f: 89 45 fc mov %eax,-0x4(%rbp)
копия result
на возвращаемое значение (return result;
)
400542: 8b 45 fc mov -0x4(%rbp),%eax
На самом деле вернуть:
400545: 5d pop %rbp
400546: c3 retq
Таким образом, вы можете видеть, что избыточное сохранение и загрузка eax
это просто потому, что сохранение и загрузка соответствуют различным утверждениям вашего исходного файла C: сохранение от result =
и нагрузка от return result;
,
Для сравнения вот оптимизированный вывод Clang (-O
):
pushq %rbp
movq %rsp, %rbp
addl %esi, %edi
movl %edi, %eax
popq %rbp
retq
Гораздо умнее: никаких манипуляций со стеком, а весь текст функции - всего лишь две инструкции addl
а также movl
, (Конечно, если вы объявите функцию static
тогда и GCC, и Clang с радостью обнаружат, что функция никогда не используется продуктивно, и просто удалят ее сразу.).