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 с радостью обнаружат, что функция никогда не используется продуктивно, и просто удалят ее сразу.).

Другие вопросы по тегам