Засел над простенькой программой:
#include <stdio.h>
void first()
{
int a=15, b=72, c=54;
};
void second()
{
int a, b, c;
printf("%d %d %d\n", a, b, c);
};
int main()
{
first();
second();
}
Интерес был в том, что вывод программы всегда был:
arturko@ARTURKO-NOTE ~/homework
$ ./test.exe
15 72 54
arturko@ARTURKO-NOTE ~/homework
$ ./test.exe
15 72 54
Из листинга программы видно, что a,b,c переменные локальные и будучи определенными в одной функции как они получают те же значений во второй?
(gdb) disas first
Dump of assembler code for function first:
0x00000001004010d0 <+0>: push rbp
0x00000001004010d1 <+1>: mov rbp,rsp
0x00000001004010d4 <+4>: sub rsp,0x10
0x00000001004010d8 <+8>: mov DWORD PTR [rbp-0x4],0xf
0x00000001004010df <+15>: mov DWORD PTR [rbp-0x8],0x48
0x00000001004010e6 <+22>: mov DWORD PTR [rbp-0xc],0x36
0x00000001004010ed <+29>: add rsp,0x10
0x00000001004010f1 <+33>: pop rbp
0x00000001004010f2 <+34>: ret
End of assembler dump.
При вызове first() следите за указателем стэка rsp. Мы выделяем 0x10 байт для хранения трех переменных в стэке. Записываем каждую переменную через 4 байта (int занимает 4 байта) определяя её значением. Потом функция first заканчивается и мы восстанавливаем адрес верхушки стэка add rsp,0x10. В итоге у нас стэк уменьшился и не содержит информации о переменных, но память под верхушкой стэка ещё содержит в себе эту информацию.
Потом запускаем функцию second:
(gdb) disas second
Dump of assembler code for function second:
0x00000001004010f3 <+0>: push rbp
0x00000001004010f4 <+1>: mov rbp,rsp
0x00000001004010f7 <+4>: sub rsp,0x30
0x00000001004010fb <+8>: mov ecx,DWORD PTR [rbp-0xc]
0x00000001004010fe <+11>: mov edx,DWORD PTR [rbp-0x8]
0x0000000100401101 <+14>: mov eax,DWORD PTR [rbp-0x4]
0x0000000100401104 <+17>: mov r9d,ecx
0x0000000100401107 <+20>: mov r8d,edx
0x000000010040110a <+23>: mov edx,eax
0x000000010040110c <+25>: lea rcx,[rip+0x1f1d] # 0x100403030
0x0000000100401113 <+32>: call 0x1004011c0 <printf>
0x0000000100401118 <+37>: nop
0x0000000100401119 <+38>: add rsp,0x30
0x000000010040111d <+42>: pop rbp
0x000000010040111e <+43>: ret
End of assembler dump.
В самом начале мы сохраняем адрес вершины стэка rsp в rbp и выделяем 0х30 байт на стэк для этой функции, а после окончания предыдущей функции rsp указывал как раз на то место в памяти стэка, под которым хранились значения переменных из прошлой функции, регистр rsp между вызовами не менялся. В итоге:
В первой функции:
0x00000001004010d8 <+8>: mov DWORD PTR [rbp-0x4],0xf
0x00000001004010df <+15>: mov DWORD PTR [rbp-0x8],0x48
0x00000001004010e6 <+22>: mov DWORD PTR [rbp-0xc],0x36
Мы помещаем в стэк по определенным адресам значения переменных. А во второй:
0x00000001004010fb <+8>: mov ecx,DWORD PTR [rbp-0xc]
0x00000001004010fe <+11>: mov edx,DWORD PTR [rbp-0x8]
0x0000000100401101 <+14>: mov eax,DWORD PTR [rbp-0x4]
Помещаем в значения регистров (которые будут использованы как наши переменные для вызова функции printf) значение переменных из первой функции которые остались в памяти и не затерлись (компилятор GCC даже не создавал отдельных переменных для этой функции, MSVC создал и присвоил те же значения).
В итоге так вот во время работы программы информация сохраняется в оперативной памяти. Мне кажется что именно ошибка в подобном коде, когда память не очищалась после функции, которая скажем считывала пароли, привела к уязвимости в OpenSSL (Heartbleed уязвимость, самая громкая ошибка этого года), которая дала возможность хакерам мониторить оперативную память (стэк) на появление в ней паролей пользователей. Но это только догадка, сопляк я ещё чтобы о таком с точностью говорить. Надеюсь, интересно было. Не только ж о вэбмордах здесь писать, люди ещё и десктоп приложения тестят )