Почему gcc генерирует дополнительный обратный адрес?
В настоящее время я изучаю основы сборки и столкнулся с чем-то странным, глядя на инструкции, сгенерированные gcc (6.1.1).
Вот источник:
#include <stdio.h>
int foo(int x, int y){
return x*y;
}
int main(){
int a = 5;
int b = foo(a, 0xF00D);
printf("0x%X\n", b);
return 0;
}
Команда, используемая для компиляции: gcc -m32 -g test.c -o test
При рассмотрении функций в GDB я получаю это:
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x080483f7 <+0>: lea ecx,[esp+0x4]
0x080483fb <+4>: and esp,0xfffffff0
0x080483fe <+7>: push DWORD PTR [ecx-0x4]
0x08048401 <+10>: push ebp
0x08048402 <+11>: mov ebp,esp
0x08048404 <+13>: push ecx
0x08048405 <+14>: sub esp,0x14
0x08048408 <+17>: mov DWORD PTR [ebp-0xc],0x5
0x0804840f <+24>: push 0xf00d
0x08048414 <+29>: push DWORD PTR [ebp-0xc]
0x08048417 <+32>: call 0x80483eb <foo>
0x0804841c <+37>: add esp,0x8
0x0804841f <+40>: mov DWORD PTR [ebp-0x10],eax
0x08048422 <+43>: sub esp,0x8
0x08048425 <+46>: push DWORD PTR [ebp-0x10]
0x08048428 <+49>: push 0x80484d0
0x0804842d <+54>: call 0x80482c0 <printf@plt>
0x08048432 <+59>: add esp,0x10
0x08048435 <+62>: mov eax,0x0
0x0804843a <+67>: mov ecx,DWORD PTR [ebp-0x4]
0x0804843d <+70>: leave
0x0804843e <+71>: lea esp,[ecx-0x4]
0x08048441 <+74>: ret
End of assembler dump.
(gdb) disas foo
Dump of assembler code for function foo:
0x080483eb <+0>: push ebp
0x080483ec <+1>: mov ebp,esp
0x080483ee <+3>: mov eax,DWORD PTR [ebp+0x8]
0x080483f1 <+6>: imul eax,DWORD PTR [ebp+0xc]
0x080483f5 <+10>: pop ebp
0x080483f6 <+11>: ret
End of assembler dump.
Меня смущает то, что он пытается сделать со стеком. Насколько я понимаю, это то, что он делает:
Сначала требуется ссылка на некоторый адрес памяти на 4 байта выше в стеке, который, насколько мне известно, должен быть переменными, передаваемыми в main, поскольку esp в данный момент указывает на адрес возврата в памяти.
Во-вторых, он выравнивает стек по границе 0 по соображениям производительности.
В-третьих, он выталкивает в новую область стека ecx + 4, что должно переводить в проталкивание адреса, к которому мы должны вернуться в стеке.
В-четвертых, он помещает старый указатель кадра в стек и устанавливает новый.
В-пятых, он помещает ecx (который все еще указывает на то, что должно быть аргументом main) в стек.
Программа делает то, что должна, и начинает процесс возврата.
Сначала он восстанавливает ecx, используя смещение -0x4 для ebp, которое должно получить доступ к первой локальной переменной.
Во-вторых, он выполняет инструкцию выхода, которая на самом деле просто устанавливает esp в ebp, а затем выталкивает ebp из стека.
Итак, теперь следующая вещь в стеке - это адрес возврата, и регистры esp и ebp должны вернуться к тому, что им нужно для правильного возврата?
Очевидно, не потому, что следующая вещь, которую он делает, это загружает esp с ecx-0x4, который, так как ecx все еще указывает на эту переменную, переданную в main, должен поместить ее по адресу адреса возврата в стеке.
Это работает просто отлично, но поднимает вопрос о том, почему он потрудился поместить адрес возврата в стек на шаге 3, так как он возвратил стек в исходное положение в конце непосредственно перед фактическим возвратом из функции.