Как распаковывается стек и определяются кадры стека - PullRequest
0 голосов
/ 02 января 2019

Допустим, у меня есть эта простая программа на C.

int my_func(int a, int b, int c) //0x4000
{
    int d = 0;
    int e = 0;
    return e+d;
}

int main()
{
    my_func(1,2,3); // 0x5000
    return 0;
}

Игнорирование того факта, что это практически весь мертвый код, который можно полностью оптимизировать. Мы скажем, что my_func () живет по адресу 0x4000 и вызывается по адресу 0x5000.

Насколько я понимаю, компилятор c (я понимаю, что он может работать по-разному в зависимости от поставщика) может:

  • нажмите c в стек
  • толкнуть b в стек
  • положить в стек
  • отправить 0x5000 в стек (обратный адрес)
  • вызов 0x4000

Тогда я предполагаю, что для доступа к нему используется sp (указатель стека) + 1. b - sp + 2, а c - sp + 3.

Поскольку d и e находятся в стеке, я думаю, наш стек теперь будет выглядеть так?

  • с
  • б
  • а
  • 0x5000
  • е

Когда мы дойдем до конца функции.

  • Затем он выталкивает и выводит из стека?
  • Тогда ... нажмите e + d? Или сохранить его в регистр, который будет использоваться после возврата?
  • Вернитесь к 0x5000, потому что это вершина стека?
  • Затем выведите адрес возврата (0x5000) и a, b и c?

Полагаю, именно поэтому старый c требовал, чтобы все переменные были объявлены в верхней части функции, чтобы компилятор мог посчитать количество всплывающих окон, необходимых для выполнения в конце функции?

Я понимаю, что он мог хранить 0x5000 в регистре, но программа на C может пройти несколько уровней глубоко во многих функциях, а регистров всего так много ...

Спасибо!

1 Ответ

0 голосов
/ 02 января 2019

В соглашении о вызовах по умолчанию для C вызывающая сторона освобождает аргумент функции после возврата из функции. Но сама функция управляет своими переменными в стеке. Например, вот ваш код в сборке без какой-либо оптимизации:

my_func:
  push ebp                      // +
  mov ebp, esp                  // These 2 lines prepare function stack
  sub esp, 16                   // reserve memory for local variables
  mov DWORD PTR [ebp-4], 0
  mov DWORD PTR [ebp-8], 0
  mov edx, DWORD PTR [ebp-8]
  mov eax, DWORD PTR [ebp-4]
  add eax, edx                  // <--return value in eax
  leave                         // return esp to what it was at start of function
  ret                           // return to caller
main:
  push ebp
  mov ebp, esp
  push 3
  push 2
  push 1
  call my_func
  add esp, 12                   // <- return esp to what it was before pushing arguments
  mov eax, 0
  leave
  ret

Как видите, в main есть add esp, 12 для возврата esp, как это было до нажатия аргументов. В my_func есть такая пара:

  push ebp
  mov ebp, esp
  sub esp, 16 // <--- size of stack
  ...
  leave
  ret

Этот набор пар используется для резервирования некоторой памяти в виде стека. leave отменяет эффект push ebp/move ebp,esp. И функция использовала ebp для доступа к своим аргументам и выделенным в стеке переменным. Возвращаемое значение всегда в eax.

Примечание о быстром выделении стека: Как видите, в функции есть инструкция add esp, 16, даже если в стеке хранится только 2 переменные типа int, общий размер которых составляет 8 байт. Это связано с тем, что размер стека выравнивается по определенным границам (по крайней мере, с параметрами компиляции по умолчанию). Если вы добавите еще 2 int переменных к my_func, эта инструкция все равно будет add esp, 16, потому что общий стек все еще находится в 16-байтовом выравнивании. Но если вы добавите третью переменную int, эта инструкция станет add esp, 32. Это выравнивание можно настроить с помощью опции -mpreferred-stack-boundary в GCC.

Кстати, все это для 32-битной компиляции кода. Напротив, вы обычно никогда не передаете аргумент через стек, передаваемый в 64-битном режиме, и передаете их через регистры. Как упомянуто в комментарии, в 64-битных аргументах только через стек передается 5-й аргумент (в соглашении о вызовах Microsoft x64 ).

Обновление:

С соглашение о вызовах по умолчанию , в среднем cdecl, которое обычно используется при компиляции кода для x86 без каких-либо опций компилятора или определенных атрибутов функции. Если вы измените вызов функции на stdcall в качестве примера, все это изменится.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...