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 в стек и читать его перед возвратом?


person olegst    schedule 25.06.2015    source источник


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

person nneonneo    schedule 25.06.2015